/*-------------------------------------------------------------------------
 *
 * xlog.c
 *		PostgreSQL 逐步日志管理器
 *
 * 写前日志 (WAL) 功能被拆分成几个源文件，除了这个文件：
 *
 * xloginsert.c - 构建 WAL 记录的函数
 * xlogrecovery.c - WAL 恢复和备用代码
 * xlogreader.c - 读取 WAL 文件和解析 WAL 记录的设施
 * xlogutils.c - WAL 重做例程的辅助函数
 *
 * 这个文件包含用于协调数据库启动和检查点，以及在系统运行时管理
 * 写前日志缓冲区的函数。
 *
 * StartupXLOG() 是启动过程的主要入口点。
 * 它协调数据库启动、执行 WAL 恢复，以及从 WAL 恢复到正常操作的迁移。
 *
 * XLogInsertRecord() 将 WAL 记录插入到 WAL 缓冲区中。
 * 大多数调用者不应直接调用此函数，而应使用 xloginsert.c 中的函数
 * 来构建 WAL 记录。可以使用 XLogFlush() 将 WAL 强制写入磁盘。
 *
 * 除此之外，还有许多其他函数用于查询当前系统状态，以及
 * 启动/停止备份。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/backend/access/transam/xlog.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <ctype.h>
#include <math.h>
#include <time.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>

#include "access/clog.h"
#include "access/commit_ts.h"
#include "access/heaptoast.h"
#include "access/multixact.h"
#include "access/rewriteheap.h"
#include "access/subtrans.h"
#include "access/timeline.h"
#include "access/transam.h"
#include "access/twophase.h"
#include "access/xact.h"
#include "access/xlog_internal.h"
#include "access/xlogarchive.h"
#include "access/xloginsert.h"
#include "access/xlogprefetcher.h"
#include "access/xlogreader.h"
#include "access/xlogrecovery.h"
#include "access/xlogutils.h"
#include "backup/basebackup.h"
#include "catalog/catversion.h"
#include "catalog/pg_control.h"
#include "catalog/pg_database.h"
#include "common/controldata_utils.h"
#include "common/file_utils.h"
#include "executor/instrument.h"
#include "miscadmin.h"
#include "pg_trace.h"
#include "pgstat.h"
#include "port/atomics.h"
#include "port/pg_iovec.h"
#include "postmaster/bgwriter.h"
#include "postmaster/startup.h"
#include "postmaster/walwriter.h"
#include "replication/logical.h"
#include "replication/origin.h"
#include "replication/slot.h"
#include "replication/snapbuild.h"
#include "replication/walreceiver.h"
#include "replication/walsender.h"
#include "storage/bufmgr.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/large_object.h"
#include "storage/latch.h"
#include "storage/pmsignal.h"
#include "storage/predicate.h"
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/reinit.h"
#include "storage/sinvaladt.h"
#include "storage/smgr.h"
#include "storage/spin.h"
#include "storage/sync.h"
#include "utils/guc.h"
#include "utils/memutils.h"
#include "utils/ps_status.h"
#include "utils/relmapper.h"
#include "utils/pg_rusage.h"
#include "utils/snapmgr.h"
#include "utils/timeout.h"
#include "utils/timestamp.h"
#ifdef FDD
#include "common/md5.h"
#include "common/scram-common.h"
#include "utils/fdb_page_encrypt.h"

char *g_fdbEncryptionKeyCommand = NULL;
char  *g_tdepass = NULL;/* 默认initdb时用户没有输入密码 */  
#endif //FDD
#ifdef FDDGM
bool g_fdballowChangeTdeEncryptType = false;
#endif

extern uint32 bootstrap_data_checksum_version;

/* 用于引导的时间线ID */  
#define BootstrapTimeLineID		1

/* 用户可设置的参数 */  
int			max_wal_size_mb = 1024; /* 1 GB */  
int			min_wal_size_mb = 80;	/* 80 MB */  
int			wal_keep_size_mb = 0;
int			XLOGbuffers = -1;
int			XLogArchiveTimeout = 0;
int			XLogArchiveMode = ARCHIVE_MODE_OFF;
char	   *XLogArchiveCommand = NULL;
bool		EnableHotStandby = false;
bool		fullPageWrites = true;
bool		wal_log_hints = false;
int			wal_compression = WAL_COMPRESSION_NONE;
char	   *wal_consistency_checking_string = NULL;
bool	   *wal_consistency_checking = NULL;
bool		wal_init_zero = true;
bool		wal_recycle = true;
bool		log_checkpoints = true;
int			sync_method = DEFAULT_SYNC_METHOD;
int			wal_level = WAL_LEVEL_MINIMAL;
int			CommitDelay = 0;	/* 预提交延迟，以微秒为单位 */  
int			CommitSiblings = 5; /* 需要休眠的并发事务数 */  
int			wal_retrieve_retry_interval = 5000;
int			max_slot_wal_keep_size_mb = -1;
int			wal_decode_buffer_size = 512 * 1024;
bool		track_wal_io_timing = false;

#ifdef WAL_DEBUG
bool		XLOG_DEBUG = false;
#endif

int			wal_segment_size = DEFAULT_XLOG_SEG_SIZE;

/*
 * 使用的WAL插入锁的数量。较高的值允许更多的插入
 * 并发发生，但增加了刷新WAL的CPU开销，
 * 需要遍历所有锁。
 */  
#define NUM_XLOGINSERT_LOCKS  8

/*
 * 距离上一个检查点的最大距离，在触发新的基于xlog的
 * 检查点之前。
*/  
int			CheckPointSegments;

/* 检查点之间的估计距离，以字节为单位 */  
static double CheckPointDistanceEstimate = 0;
static double PrevCheckPointDistance = 0;

/*
 * GUC 支持
 */
const struct config_enum_entry sync_method_options[] = {
	{"fsync", SYNC_METHOD_FSYNC, false},
#ifdef HAVE_FSYNC_WRITETHROUGH
	{"fsync_writethrough", SYNC_METHOD_FSYNC_WRITETHROUGH, false},
#endif
#ifdef HAVE_FDATASYNC
	{"fdatasync", SYNC_METHOD_FDATASYNC, false},
#endif
#ifdef OPEN_SYNC_FLAG
	{"open_sync", SYNC_METHOD_OPEN, false},
#endif
#ifdef OPEN_DATASYNC_FLAG
	{"open_datasync", SYNC_METHOD_OPEN_DSYNC, false},
#endif
	{NULL, 0, false}
};


/*
 * 虽然只文档化了“开启”、“关闭”和“始终”，
 * 但我们接受所有可能的“开启”和“关闭”的变体。
 */  
const struct config_enum_entry archive_mode_options[] = {
	{"always", ARCHIVE_MODE_ALWAYS, false},
	{"on", ARCHIVE_MODE_ON, false},
	{"off", ARCHIVE_MODE_OFF, false},
	{"true", ARCHIVE_MODE_ON, true},
	{"false", ARCHIVE_MODE_OFF, true},
	{"yes", ARCHIVE_MODE_ON, true},
	{"no", ARCHIVE_MODE_OFF, true},
	{"1", ARCHIVE_MODE_ON, true},
	{"0", ARCHIVE_MODE_OFF, true},
	{NULL, 0, false}
};

/*
 * 当前检查点的统计信息收集在这个全局结构中。
 * 由于只有检查点程序或独立后端可以执行
 * 检查点，因此在正常后端中该结构将未使用。
 */  
CheckpointStatsData CheckpointStats;

/*
 * 在恢复过程中，lastFullPageWrites跟踪
 * 重新播放的WAL记录指示的full_page_writes。它的初始化
 * 值为恢复起始检查点记录指示的full_page_writes，
 * 然后在每次重新播放XLOG_FPW_CHANGE记录时更新。
 */  
static bool lastFullPageWrites;

/*
 * 在共享内存中由SharedRecoveryState跟踪的状态的本地副本，
 * 如果SharedRecoveryState为RECOVERY_STATE_DONE则为false。
 * True实际上意味着“未知，需要检查共享状态”。
 */  
static bool LocalRecoveryInProgress = true;

/*
 * XLogInsertAllowed()的本地状态：
 *		1：无条件允许插入XLOG
 *		0：无条件不允许插入XLOG
 *		-1：必须检查RecoveryInProgress(); 直到它为false时不允许
 * 大多数进程以-1开始，在看到恢复未进行后过渡到1。
 * 但我们也可以在特殊情况下强制该值。
 * XLogInsertAllowed()中的编码依赖于这两个状态
 * 在数值上与布尔值true和false相同。
 */  
static int	LocalXLogInsertAllowed = -1;

/*
 * ProcLastRecPtr指向当前后端插入的最后XLOG记录的开始。
 * 它会在所有插入中更新。XactLastRecEnd指向
 * 最后记录的结束+1，当我们结束一个顶级事务时重置，
 * 或开始一个新的事务；因此它可以用来判断
 * 当前事务是否创建了任何XLOG记录。
 *
 * 在并行模式下，这可能不是完全最新的。当提交时，
 * 事务可以假设这覆盖了用户后端或在
 * 事务期间的任何时刻存在的任何并行工作者写入的所有
 * xlog记录。但在回滚或仍在并行模式下时，其他
 * 并行后端可能在存储在这里的值之后的较大LSN处写入了WAL记录。
 * 并行领导者在WaitForParallelWorkersToFinish中必要时推进自己的副本。
 */  
XLogRecPtr	ProcLastRecPtr = InvalidXLogRecPtr;
XLogRecPtr	XactLastRecEnd = InvalidXLogRecPtr;
XLogRecPtr	XactLastCommitEnd = InvalidXLogRecPtr;

/*
 * RedoRecPtr 是这个后端对 REDO 记录指针的本地副本
 * （几乎但不完全等同于指向最近的 CHECKPOINT 记录的指针）。我们在
 * 可以安全地这样做时（即，当我们持有插入锁时）从共享内存副本
 * XLogCtl->Insert.RedoRecPtr 更新它。有关详细信息，请参见
 * XLogInsertRecord。如果我们持有 info_lck， 也允许从
 * XLogCtl->RedoRecPtr 更新；参见 GetRedoRecPtr。
 *
 * 注意：使用此变量的代码必须准备好应对
 * 它可能被任意的数据更新过时的可能性，以及
 * 它可能被设置为 InvalidXLogRecPtr 的可能性。我们过去
 * 是通过第一次调用 RecoveryInProgress() 的副作用来初始化它，
 * 这意味着大多数可能使用它的代码可以假设它有一个
 * 真正，但可能过时的值。但情况不再如此。
 */
static XLogRecPtr RedoRecPtr;

/*
 * doPageWrites 是这个后端对 (forcePageWrites ||
 * fullPageWrites) 的本地副本。它与 RedoRecPtr 一起使用
 * 来决定是否需要获取页面的全页图像。
 *
 * 注意：最初这是 false，且不能保证在第一次使用之前
 * 会被初始化为任何其他值。任何使用它的代码必须在获得
 * WALInsertLock 后重新检查该值，并在发现之前的值不准确时
 * 适当地响应。
 */
static bool doPageWrites;

/*----------
 * XLOG控制的共享内存数据结构
 *
 * LogwrtRqst表示我们需要写入和/或fsync日志的字节位置（该点之前的所有记录必须被写入或fsynced）。
 * LogwrtResult表示我们已经写入/ fsynced的字节位置。
 * 这些结构是相同的，但分别声明以表示其稍有不同的功能。
 *
 * 要读取XLogCtl->LogwrtResult，必须持有info_lck或
 * WALWriteLock。要更新它，您需要同时持有两个锁。
 * 这种安排的目的是，已经持有WALWriteLock的代码可以检查该值，而不需要同时获取info_lck。
 * 除了共享变量外，每个后端都有一个LogwrtResult的私有副本，
 * 在方便时更新。
 *
 * 请求的记账更简单：有一个共享的XLogCtl->LogwrtRqst
 *（由info_lck保护），但我们不需要缓存任何副本。
 *
 * info_lck仅持有足够长的时间来读取/更新受保护的变量，
 * 所以它是一个普通的自旋锁。其他锁持有的时间更长（可能
 * 在I/O操作期间），因此我们使用LWLocks。
 * 这些锁是：
 *
 * WALBufMappingLock：必须在WAL缓冲区缓存中替换页面时持有。
 * 它仅在初始化和更改映射时持有。如果正在被替换的缓冲区的内容尚未被写入，
 * 则在写入时释放映射锁，写入完成后重新获取。
 *
 * WALWriteLock：必须在将WAL缓冲区写入磁盘时持有（XLogWrite或
 * XLogFlush）。
 *
 * ControlFileLock：必须在读取/更新控制文件或创建
 * 新日志文件时持有。
 *
 *----------
 */

typedef struct XLogwrtRqst
{
	XLogRecPtr	Write;			/* 最后一个字节 + 1 写出 */
	XLogRecPtr	Flush;			/* 最后一个字节 + 1 刷新 */
} XLogwrtRqst;

typedef struct XLogwrtResult
{
	XLogRecPtr	Write;			/* 最后一个字节 + 1 已写出 */
	XLogRecPtr	Flush;			/* 最后一个字节 + 1 已刷新 */
} XLogwrtResult;

/*
 * 插入WAL受限于少量固定数量的WAL插入锁。要插入WAL，必须持有其中一个锁——不管是哪一个。为了锁定其他并发插入，必须持有其中之一。每个WAL插入锁由轻量级锁和一个指示插入进度的值（insertingAt）组成。
 *
 * 当进程想要将WAL从内存缓冲区刷新到磁盘时，会读取insertingAt值，以检查进程即将写入的区域的所有插入是否已完成。您可以简单地等待所有当前进行中的插入完成，但insertingAt指示符允许您忽略WAL中之后的插入，因此您只需等待修改您即将写出的缓冲区的插入。
 *
 * 这不仅仅是一个优化。如果所有WAL缓冲区都是脏的，持有WAL插入锁的插入者可能需要逐出一个旧的WAL缓冲区，这需要刷新WAL。如果插入者可能不必要地在另一个插入者上阻塞，当两个持有WAL插入锁的插入者等待彼此完成插入时，可能会产生死锁。
 *
 * 小的WAL记录在不跨越页面边界时不会更新值，WAL记录只是被复制到页面并释放锁。但为了避免上面解释的死锁场景，在持有插入锁时总是在休眠之前更新指示符。
 *
 * lastImportantAt包含使用给定锁插入的最后一个重要WAL记录的LSN。此值用于检测自上次执行某个操作（例如检查点）以来是否存在重要的WAL活动——如果没有，允许不重复该操作。LSN在所有插入中更新，除非设置了XLOG_MARK_UNIMPORTANT标志。lastImportantAt从不清除，仅被更新的记录LSN覆盖。直接在WALInsertLock中跟踪WAL活动的好处是不需要任何额外的锁来更新值。
 */
typedef struct
{
	LWLock		lock;
	XLogRecPtr	insertingAt;
	XLogRecPtr	lastImportantAt;
} WALInsertLock;

/*
 * 所有WAL插入锁作为共享内存中的数组分配。我们强制数组的步幅为2的幂，这样在索引时节省了一些周期，但更重要的是，确保了各个槽不会跨越缓存行边界。（当然，我们还必须确保数组起始地址适当对齐。）
 */
typedef union WALInsertLockPadded
{
	WALInsertLock l;
	char		pad[PG_CACHE_LINE_SIZE];
} WALInsertLockPadded;

/*
 * 运行备份的会话状态，用于在可调用SQL的函数中进行启动和停止备份的完整性检查。
 */
static SessionBackupState sessionBackupState = SESSION_BACKUP_NONE;

/*
 * WAL插入的共享状态数据。
 */
typedef struct XLogCtlInsert
{
	slock_t		insertpos_lck;	/* 保护CurrBytePos和PrevBytePos */

	/*
	 * CurrBytePos 是保留 WAL 的末尾。下一个记录将插入到该位置。PrevBytePos 是之前插入（或者说，保留）记录的起始位置 - 它被复制到下一个记录的 prev-link。它们作为“可用字节位置”而不是 XLogRecPtrs 存储（参见 XLogBytePosToRecPtr()）。
	 */
	uint64		CurrBytePos;
	uint64		PrevBytePos;

	/*
	 * 确保上述竞争激烈的自旋锁和字节位置在它们自己的缓存行上。特别是，下面的 RedoRecPtr 和完整页面写入变量应该在不同的缓存行上。它们在每次 WAL 插入时被读取，但很少更新，我们不希望这些读取抢占包含 Curr/PrevBytePos 的缓存行。
	 */
	char		pad[PG_CACHE_LINE_SIZE];

	/*
	 * fullPageWrites 是所有后端用来确定是否将完整页面图像写入 WAL 的权威值。这个共享值，而不是进程本地的 fullPageWrites，是必需的，因为当 full_page_writes 被 SIGHUP 更改时，我们必须在它实际影响后端的 WAL 日志记录之前将其记录到 WAL。检查点在启动时或 SIGHUP 后设置。
	 *
	 * 要读取这些字段，您必须保持插入锁。要修改它们，您必须持有所有锁。
	 */
	XLogRecPtr	RedoRecPtr;		/* 当前插入的重做点 */
	bool		forcePageWrites;	/* 强制对 PITR 的完整页面写入？ */
	bool		fullPageWrites;

	/*
	 * runningBackups 是一个计数器，指示当前正在进行的备份数量。当 runningBackups 非零时，forcePageWrites 被设置为 true。lastBackupStart 是最新检查点重做位置，用作在线备份的起始点。
	 */
	int			runningBackups;
	XLogRecPtr	lastBackupStart;

	/*
	 * WAL 插入锁。
	 */
	WALInsertLockPadded *WALInsertLocks;
} XLogCtlInsert;

/*
 * XLOG 的共享内存状态总计。
 */
typedef struct XLogCtlData
{
	XLogCtlInsert Insert;

	/* 受 info_lck 保护： */
	XLogwrtRqst LogwrtRqst;
	XLogRecPtr	RedoRecPtr;		/* Insert->RedoRecPtr 的最近副本 */
	FullTransactionId ckptFullXid;	/* 最新检查点的 nextXid */
	XLogRecPtr	asyncXactLSN;	/* 最新的异步提交/回滚的 LSN */
	XLogRecPtr	replicationSlotMinLSN;	/* 任何插槽所需的最旧 LSN */

	XLogSegNo	lastRemovedSegNo;	/* 最新移除/回收的 XLOG 段 */

	/* 假的 LSN 计数器，供未记录的关系使用。由 ulsn_lck 保护。 */
	XLogRecPtr	unloggedLSN;
	slock_t		ulsn_lck;

	/* 最近 xlog 段切换的时间和 LSN。由 WALWriteLock 保护。 */
	pg_time_t	lastSegSwitchTime;
	XLogRecPtr	lastSegSwitchLSN;

	/*
	 * 受 info_lck 和 WALWriteLock 保护（您必须持有任一锁以读取它，但要更新必须持有两个锁）
	 */
	XLogwrtResult LogwrtResult;

	/*
	 * 缓存中初始化的最新页面（最后一个字节位置 + 1）。
	 *
	 * 要更改缓冲区的身份（和 InitializedUpTo），您需要持有 WALBufMappingLock。要更改仍然脏的缓冲区的身份，必须先写出旧页面，为此您需要 WALWriteLock，并且您需要确保没有进行中的插入操作可以通过调用
	 * WaitXLogInsertionsToFinish() 来确认。
	 */
	XLogRecPtr	InitializedUpTo;

	/*
	 * 这些值在启动后不会改变，尽管指向的页面和 xlblocks 值肯定会改变。xlblocks 值由 WALBufMappingLock 保护。
	 */
	char	   *pages;			/* 未写入 XLOG 页的缓冲区 */
	XLogRecPtr *xlblocks;		/* 第一个字节 ptr-s + XLOG_BLCKSZ */
	int			XLogCacheBlck;	/* 最高分配的 xlog 缓冲区索引 */

	/*
	 * InsertTimeLineID 是正在插入和刷新新 WAL 的时间线。
	 * 在恢复期间为零，一旦设置后不会更改。
	 *
	 * 如果我们在系统启动时创建一个新的时间线，
	 * PrevTimeLineID 是我们分叉的旧时间线的 ID。
	 * 否则，它等于 InsertTimeLineID。
	 */
	TimeLineID	InsertTimeLineID;
	TimeLineID	PrevTimeLineID;

	/*
	 * SharedRecoveryState 表示我们是否仍处于崩溃或归档
	 * 恢复中。由 info_lck 保护。
	 */
	RecoveryState SharedRecoveryState;

	/*
	 * InstallXLogFileSegmentActive 表示检查点程序是否应该
	 * 通过回收和/或 PreallocXlogFiles() 来安排未来的段。
	 * 由 ControlFileLock 保护。只有启动进程可以更改它。
	 * 如果为 true，任何人都可以使用 InstallXLogFileSegment()。
	 * 如果为 false，启动进程拥有独占权利来安装段，通过从
	 * 归档中读取并可能替换现有文件。
	 */
	bool		InstallXLogFileSegmentActive;

	/*
	 * WalWriterSleeping 表示 WAL 写入器是否当前处于
	 * 低功耗模式（因此如果发生异步提交，则应进行提示）。
	 * 由 info_lck 保护。
	 */
	bool		WalWriterSleeping;

	/*
	 * 在恢复期间，我们在这里保留最新检查点记录的副本。
	 * lastCheckPointRecPtr 指向检查点记录的开始，而
	 * lastCheckPointEndPtr 指向检查点记录的结束+1。
	 * 当检查点程序想要创建重启点时使用。
	 *
	 * 由 info_lck 保护。
	 */
	XLogRecPtr	lastCheckPointRecPtr;
	XLogRecPtr	lastCheckPointEndPtr;
	CheckPoint	lastCheckPoint;

	/*
	 * lastFpwDisableRecPtr 指向最后一个重放的
	 * XLOG_FPW_CHANGE 记录的开始，该记录指示 full_page_writes 已禁用。
	 */
	XLogRecPtr	lastFpwDisableRecPtr;

	slock_t		info_lck;		/* 锁定上述共享变量 */
} XLogCtlData;

static XLogCtlData *XLogCtl = NULL;

/* XLogCtl->Insert.WALInsertLocks 的私有副本，方便起见 */
static WALInsertLockPadded *WALInsertLocks = NULL;

/*
 * 我们在共享内存中维护 pg_control 的映像。
 */
static ControlFileData *ControlFile = NULL;

/*
 * 计算 'endptr' 之后页面上剩余的空间。
 * 注意多次评估！
 */
#define INSERT_FREESPACE(endptr)	\
	(((endptr) % XLOG_BLCKSZ == 0) ? 0 : (XLOG_BLCKSZ - (endptr) % XLOG_BLCKSZ))

/* 宏以推进到下一个缓冲区索引。 */
#define NextBufIdx(idx)		\
		(((idx) == XLogCtl->XLogCacheBlck) ? 0 : ((idx) + 1))

/*
 * XLogRecPtrToBufIdx 返回包含 'recptr' 的 WAL 缓冲区的索引，
 * 或如果它在缓存中，将会包含该页面。
 */
#define XLogRecPtrToBufIdx(recptr)	\
	(((recptr) / XLOG_BLCKSZ) % (XLogCtl->XLogCacheBlck + 1))

/*
 * 这些是可用于 WAL 数据的 WAL 页面中的字节数。
 */
#define UsableBytesInPage (XLOG_BLCKSZ - SizeOfXLogShortPHD)

/*
 * 将以兆字节为单位测量的 GUC 值转换为等效的段数。
 * 向下舍入。
 */
#define ConvertToXSegs(x, segsize)	XLogMBVarToSegs((x), (segsize))

/* 可用于 WAL 数据的 WAL 段中的字节数。 */
static int	UsableBytesInSegment;

/*
 * 私有的，可能过时的共享 LogwrtResult 副本。
 * 请参见上述讨论。
 */
static XLogwrtResult LogwrtResult = {0, 0};

/*
 * openLogFile 是 -1 或用于打开日志文件段的内核 FD。
 * openLogSegNo 标识段，openLogTLI 标识相应的 TLI。
 * 这些变量仅用于写入 XLOG，因此通常会引用
 * 活动段。
 *
 * 注意：调用 Reserve/ReleaseExternalFD 来跟踪该 FD 的消费。
 */
static int	openLogFile = -1;
static XLogSegNo openLogSegNo = 0;
static TimeLineID openLogTLI = 0;

/*
 * 控制文件中等效字段的本地副本。 在运行
 * 崩溃恢复时，LocalMinRecoveryPoint 被设置为 InvalidXLogRecPtr，因为我们
 * 期望重放所有可用的 WAL，并且 updateMinRecoveryPoint 被切换为 false，以防止在重放记录时进行任何更新。
 * 只要崩溃恢复运行，这些值就会保持一致。
 */
static XLogRecPtr LocalMinRecoveryPoint;
static TimeLineID LocalMinRecoveryPointTLI;
static bool updateMinRecoveryPoint = true;

/* 对于 WALInsertLockAcquire/Release 函数 */
static int	MyLockNo = 0;
static bool holdingAllLocks = false;

#ifdef WAL_DEBUG
static MemoryContext walDebugCxt = NULL;
#endif

static void fc_CleanupAfterArchiveRecovery(TimeLineID fc_EndOfLogTLI,
										XLogRecPtr fc_EndOfLog,
										TimeLineID fc_newTLI);
static void fc_CheckRequiredParameterValues(void);
static void fc_XLogReportParameters(void);
static int	fc_LocalSetXLogInsertAllowed(void);
static void fc_CreateEndOfRecoveryRecord(void);
static XLogRecPtr fc_CreateOverwriteContrecordRecord(XLogRecPtr fc_aborted_lsn,
												  XLogRecPtr fc_missingContrecPtr,
												  TimeLineID fc_newTLI);
static void fc_CheckPointGuts(XLogRecPtr fc_checkPointRedo, int fc_flags);
static void fc_KeepLogSeg(XLogRecPtr fc_recptr, XLogSegNo *fc_logSegNo);
static XLogRecPtr fc_XLogGetReplicationSlotMinimumLSN(void);

static void fc_AdvanceXLInsertBuffer(XLogRecPtr fc_upto, TimeLineID fc_tli,
								  bool fc_opportunistic);
static void fc_XLogWrite(XLogwrtRqst fc_WriteRqst, TimeLineID fc_tli, bool fc_flexible);
static bool fc_InstallXLogFileSegment(XLogSegNo *fc_segno, char *fc_tmppath,
								   bool fc_find_free, XLogSegNo fc_max_segno,
								   TimeLineID fc_tli);
static void fc_XLogFileClose(void);
static void fc_PreallocXlogFiles(XLogRecPtr fc_endptr, TimeLineID fc_tli);
static void fc_RemoveTempXlogFiles(void);
static void fc_RemoveOldXlogFiles(XLogSegNo fc_segno, XLogRecPtr fc_lastredoptr,
							   XLogRecPtr fc_endptr, TimeLineID fc_insertTLI);
static void fc_RemoveXlogFile(const char *fc_segname, XLogSegNo fc_recycleSegNo,
						   XLogSegNo *fc_endlogSegNo, TimeLineID fc_insertTLI);
static void fc_UpdateLastRemovedPtr(char *fc_filename);
static void fc_ValidateXLOGDirectoryStructure(void);
static void fc_CleanupBackupHistory(void);
static void fc_UpdateMinRecoveryPoint(XLogRecPtr fc_lsn, bool fc_force);
static bool fc_PerformRecoveryXLogAction(void);
static void fc_InitControlFile(uint64 fc_sysidentifier);
static void fc_WriteControlFile(void);
static void fc_ReadControlFile(void);
static void fc_UpdateControlFile(void);
static char *fc_str_time(pg_time_t fc_tnow);

static void fc_pg_backup_start_callback(int fc_code, Datum fc_arg);

static int	fc_get_sync_bit(int fc_method);

static void fc_CopyXLogRecordToWAL(int fc_write_len, bool fc_isLogSwitch,
								XLogRecData *fc_rdata,
								XLogRecPtr fc_StartPos, XLogRecPtr fc_EndPos,
								TimeLineID fc_tli);
static void fc_ReserveXLogInsertLocation(int fc_size, XLogRecPtr *fc_StartPos,
									  XLogRecPtr *fc_EndPos, XLogRecPtr *fc_PrevPtr);
static bool fc_ReserveXLogSwitch(XLogRecPtr *fc_StartPos, XLogRecPtr *fc_EndPos,
							  XLogRecPtr *fc_PrevPtr);
static XLogRecPtr fc_WaitXLogInsertionsToFinish(XLogRecPtr fc_upto);
static char *fc_GetXLogBuffer(XLogRecPtr fc_ptr, TimeLineID fc_tli);
static XLogRecPtr fc_XLogBytePosToRecPtr(uint64 fc_bytepos);
static XLogRecPtr fc_XLogBytePosToEndRecPtr(uint64 fc_bytepos);
static uint64 fc_XLogRecPtrToBytePos(XLogRecPtr fc_ptr);

static void fc_WALInsertLockAcquire(void);
static void fc_WALInsertLockAcquireExclusive(void);
static void fc_WALInsertLockRelease(void);
static void fc_WALInsertLockUpdateInsertingAt(XLogRecPtr fc_insertingAt);

/*
 * 插入一个由已经构造的数据块链表示的 XLOG 记录。
 * 这是一个低级例程；要构造 WAL 记录头和数据，请使用
 * xloginsert.c 中的更高级例程。
 *
 * 如果 'fpw_lsn' 是有效的，则它是此
 * WAL 记录适用的页面中，未作为完整页面图像包含在记录中的最旧 LSN。
 * 如果 fpw_lsn <= RedoRecPtr，则该函数不执行
 * 插入并返回 InvalidXLogRecPtr。调用者可以重新计算
 * 哪些页面需要完整页面图像，并重试。 如果 fpw_lsn 无效，则
 * 记录始终会被插入。
 *
 * 'flags' 提供了更深入的控制，以插入的记录。请参见
 * XLogSetRecordFlags() 了解详细信息。
 *
 * 'topxid_included' 表示顶级事务 ID 是否与
 * 当前子事务一起记录。请参见 XLogRecordAssemble()。
 *
 * 链中的第一个 XLogRecData 必须是记录头，并且其
 * 数据必须是 MAXALIGN 的。 XLogInsertRecord 填充 xl_prev 和
 * xl_crc 字段在头部，头部的其余部分必须已经由
 * 调用者填充。
 *
 * 返回 XLOG 指针到记录的末尾（下一个记录的开始）。
 * 这可以作为受已记录操作影响的数据页面的 LSN。
 * （LSN 是 XLOG 点，直到该 XLOG 必须刷新到磁盘
 * 之前，数据页面才能被写出。这实现了基本的
 * WAL 规则“先写日志，再写数据”。）
 */
XLogRecPtr XLogInsertRecord(XLogRecData *fc_rdata,
				 XLogRecPtr fc_fpw_lsn,
				 uint8 fc_flags,
				 int fc_num_fpi,
				 bool fc_topxid_included)
{
	XLogCtlInsert *fc_Insert = &XLogCtl->Insert;
	pg_crc32c	fc_rdata_crc;
	bool		fc_inserted;
	XLogRecord *fc_rechdr = (XLogRecord *) fc_rdata->data;
	uint8		fc_info = fc_rechdr->xl_info & ~XLR_INFO_MASK;
	bool		fc_isLogSwitch = (fc_rechdr->xl_rmid == RM_XLOG_ID &&
							   fc_info == XLOG_SWITCH);
	XLogRecPtr	fc_StartPos;
	XLogRecPtr	fc_EndPos;
	bool		fc_prevDoPageWrites = doPageWrites;
	TimeLineID	fc_insertTLI;

	/* 我们假设记录头的所有内容都在第一个块中 */
	Assert(fc_rdata->len >= SizeOfXLogRecord);

	/* 检查我们是否应该在这里 */
	if (!XLogInsertAllowed())
		elog(ERROR, "cannot make new WAL entries during recovery");

	/*
	 * 鉴于我们不在恢复中，InsertTimeLineID 已设置且无法
	 * 更改，因此我们可以在不加锁的情况下读取它。
	 */
	fc_insertTLI = XLogCtl->InsertTimeLineID;

	/*----------
	 *
	 * 我们现在已经完成了所有可以在不持有锁或修改共享状态的情况下进行的准备工作。从这里开始，将新的WAL记录插入共享WAL缓冲区缓存是一个两步的过程：
	 *
	 * 1. 从WAL中保留合适的空间。当前保留空间的首部保存在Insert->CurrBytePos中，并由insertpos_lck保护。
	 *
	 * 2. 将记录复制到保留的WAL空间。这涉及查找包含保留空间的正确WAL缓冲区，并在原位复制记录。这可以在多个进程中并发完成。
	 *
	 * 为了跟踪哪些插入仍在进行中，每个并发插入者获取一个插入锁。除了仅指示插入正在进行外，锁还告诉其他人插入者已经进展到什么程度。插入锁的数量是固定的，由NUM_XLOGINSERT_LOCKS决定。当插入者跨越页面边界时，它会更新锁中存储的值，以指示它已经插入的量，从而允许前一个缓冲区被刷新。
	 *
	 * 持有插入锁还保护RedoRecPtr和fullPageWrites在插入完成之前不会发生变化。
	 *
	 * 第2步通常可以完全并行完成。如果所需的WAL页面尚未初始化，则必须抓取WALBufMappingLock进行初始化，但WAL写入器会在插入之前尝试这样做，以避免在关键路径中发生这种情况。
	 *
	 *----------
	 */
	START_CRIT_SECTION();
	if (fc_isLogSwitch)
		fc_WALInsertLockAcquireExclusive();
	else
		fc_WALInsertLockAcquire();

	/*
	 * 检查我的RedoRecPtr副本是否过期。如果是，可能需要返回并让调用者重新计算所有内容。这只能在检查点之后发生，因此在这种情况下最好慢一点，其余时间快一点。
	 *
	 * 还要检查fullPageWrites或forcePageWrites是否刚刚开启；如果我们还没有进行全页写入，则返回并重新计算。
	 *
	 * 如果我们没有进行全页写入，则RedoRecPtr实际上并不影响XLOG记录的内容，因此我们将更新我们的本地副本，但不强制重新计算。（如果doPageWrites刚刚关闭，我们可以在没有全页的情况下重新计算记录，但我们选择不去麻烦。）
	 */
	if (RedoRecPtr != fc_Insert->RedoRecPtr)
	{
		Assert(RedoRecPtr < fc_Insert->RedoRecPtr);
		RedoRecPtr = fc_Insert->RedoRecPtr;
	}
	doPageWrites = (fc_Insert->fullPageWrites || fc_Insert->forcePageWrites);

	if (doPageWrites &&
		(!fc_prevDoPageWrites ||
		 (fc_fpw_lsn != InvalidXLogRecPtr && fc_fpw_lsn <= RedoRecPtr)))
	{
		/*
		 * 哎呀，某个缓冲区现在需要进行备份，但调用者没有备份。重新开始。
		 */
		fc_WALInsertLockRelease();
		END_CRIT_SECTION();
		return InvalidXLogRecPtr;
	}

	/*
	 * 在WAL中为记录保留空间。这也设置了xl_prev指针。
	 */
	if (fc_isLogSwitch)
		fc_inserted = fc_ReserveXLogSwitch(&fc_StartPos, &fc_EndPos, &fc_rechdr->xl_prev);
	else
	{
		fc_ReserveXLogInsertLocation(fc_rechdr->xl_tot_len, &fc_StartPos, &fc_EndPos,
								  &fc_rechdr->xl_prev);
		fc_inserted = true;
	}

	if (fc_inserted)
	{
		/*
		 * 现在xl_prev已经填充，计算记录头的CRC。
		 */
		fc_rdata_crc = fc_rechdr->xl_crc;
		COMP_CRC32C(fc_rdata_crc, fc_rechdr, offsetof(XLogRecord, xl_crc));
		FIN_CRC32C(fc_rdata_crc);
		fc_rechdr->xl_crc = fc_rdata_crc;

		/*
		 * 所有记录数据，包括头部，现在准备好插入了。在保留的空间中复制记录。
		 */
		fc_CopyXLogRecordToWAL(fc_rechdr->xl_tot_len, fc_isLogSwitch, fc_rdata,
							fc_StartPos, fc_EndPos, fc_insertTLI);

		/*
		 * 除非记录被标记为不重要，否则更新当前插槽中最后一个重要记录的LSN。当持有所有锁时，仅更新第一个。
		 */
		if ((fc_flags & XLOG_MARK_UNIMPORTANT) == 0)
		{
			int			fc_lockno = holdingAllLocks ? 0 : MyLockNo;

			WALInsertLocks[fc_lockno].l.lastImportantAt = fc_StartPos;
		}
	}
	else
	{
		/*
		 * 这是一个xlog-switch记录，但当前插入位置已经正好位于一个段的开始，因此无需进行任何操作。
		 */
	}

	/*
	 * 完成！让其他人知道我们已经结束。
	 */
	fc_WALInsertLockRelease();

	END_CRIT_SECTION();

	MarkCurrentTransactionIdLoggedIfAny();

	/*
	 * 标记顶级事务ID已经记录（如有必要），以便我们不应在当前子事务中再次尝试记录它。
	 */
	if (fc_topxid_included)
		MarkSubxactTopXidLogged();

	/*
	 * 如果我们跨越了页面边界，更新共享LogwrtRqst.Write。
	 */
	if (fc_StartPos / XLOG_BLCKSZ != fc_EndPos / XLOG_BLCKSZ)
	{
		SpinLockAcquire(&XLogCtl->info_lck);
		/* 使全球请求向前推进以包括新块 */
		if (XLogCtl->LogwrtRqst.Write < fc_EndPos)
			XLogCtl->LogwrtRqst.Write = fc_EndPos;
		/* 在我有机会的时候更新本地结果副本 */
		LogwrtResult = XLogCtl->LogwrtResult;
		SpinLockRelease(&XLogCtl->info_lck);
	}

	/*
	 * 如果这是一个XLOG_SWITCH记录，刷新记录和填充段其余部分的空填充空间，并执行段末操作（例如，通知归档器）。
	 */
	if (fc_isLogSwitch)
	{
		TRACE_POSTGRESQL_WAL_SWITCH();
		XLogFlush(fc_EndPos);

		/*
		 * 尽管我们为自己保留了段的其余部分，这反映在EndPos中，但我们返回的指针仅指向xlog-switch记录的末尾。
		 */
		if (fc_inserted)
		{
			fc_EndPos = fc_StartPos + SizeOfXLogRecord;
			if (fc_StartPos / XLOG_BLCKSZ != fc_EndPos / XLOG_BLCKSZ)
			{
				uint64		fc_offset = XLogSegmentOffset(fc_EndPos, wal_segment_size);

				if (fc_offset == fc_EndPos % XLOG_BLCKSZ)
					fc_EndPos += SizeOfXLogLongPHD;
				else
					fc_EndPos += SizeOfXLogShortPHD;
			}
		}
	}

#ifdef WAL_DEBUG
	if (XLOG_DEBUG)
	{
		static XLogReaderState *fc_debug_reader = NULL;
		XLogRecord *fc_record;
		DecodedXLogRecord *fc_decoded;
		StringInfoData fc_buf;
		StringInfoData fc_recordBuf;
		char	   *fc_errormsg = NULL;
		MemoryContext fc_oldCxt;

		fc_oldCxt = MemoryContextSwitchTo(walDebugCxt);

		initStringInfo(&fc_buf);
		appendStringInfo(&fc_buf, "INSERT @ %X/%X: ", LSN_FORMAT_ARGS(fc_EndPos));

		/*
		 * 我们必须将WAL记录数据从XLogRecData条目拼凑在一起，以便将其作为一个连续块传递给rm_desc函数。
		 */
		initStringInfo(&fc_recordBuf);
		for (; fc_rdata != NULL; fc_rdata = fc_rdata->next)
			appendBinaryStringInfo(&fc_recordBuf, fc_rdata->data, fc_rdata->len);

		/* 我们还需要临时空间来解码记录。 */
		fc_record = (XLogRecord *) fc_recordBuf.data;
		fc_decoded = (DecodedXLogRecord *)
			palloc(DecodeXLogRecordRequiredSpace(fc_record->xl_tot_len));

		if (!fc_debug_reader)
			fc_debug_reader = XLogReaderAllocate(wal_segment_size, NULL,
											  XL_ROUTINE(.page_read = NULL,
														 .segment_open = NULL,
														 .segment_close = NULL),
											  NULL);
		if (!fc_debug_reader)
		{
			appendStringInfoString(&fc_buf, "error decoding record: out of memory while allocating a WAL reading processor");
		}
		else if (!DecodeXLogRecord(fc_debug_reader,
								   fc_decoded,
								   fc_record,
								   fc_EndPos,
								   &fc_errormsg))
		{
			appendStringInfo(&fc_buf, "error decoding record: %s",
							 fc_errormsg ? fc_errormsg : "no error message");
		}
		else
		{
			appendStringInfoString(&fc_buf, " - ");

			fc_debug_reader->record = fc_decoded;
			xlog_outdesc(&fc_buf, fc_debug_reader);
			fc_debug_reader->record = NULL;
		}
		elog(LOG, "%s", fc_buf.data);

		pfree(fc_decoded);
		pfree(fc_buf.data);
		pfree(fc_recordBuf.data);
		MemoryContextSwitchTo(fc_oldCxt);
	}
#endif

	/*
	 * 更新我们的全局变量
	 */
	ProcLastRecPtr = fc_StartPos;
	XactLastRecEnd = fc_EndPos;

	/* 向仪表仪器报告WAL流量。 */
	if (fc_inserted)
	{
		pgWalUsage.wal_bytes += fc_rechdr->xl_tot_len;
		pgWalUsage.wal_records++;
		pgWalUsage.wal_fpi += fc_num_fpi;
	}

	return fc_EndPos;
}

/*
 * 为给定大小的记录从WAL预留适当的空间。
 * *StartPos设置为保留部分的开始，*EndPos设置为其结束+1。*PrevPtr设置为前一记录的开始；它用于设置此记录的xl_prev。
 *
 * 这是XLogInsert中性能关键的部分，必须在后端之间进行序列化。其余部分可以大多数并行进行。尽量将此部分保持尽可能短，insertpos_lck在繁忙系统上可能会严重争用。
 *
 * 注意：此处的空间计算必须与CopyXLogRecordToWAL中的代码匹配，
 * 在那里我们实际上将记录复制到保留空间。
 */
static void fc_ReserveXLogInsertLocation(int fc_size, XLogRecPtr *fc_StartPos, XLogRecPtr *fc_EndPos,
						  XLogRecPtr *fc_PrevPtr)
{
	XLogCtlInsert *fc_Insert = &XLogCtl->Insert;
	uint64		fc_startbytepos;
	uint64		fc_endbytepos;
	uint64		fc_prevbytepos;

	fc_size = MAXALIGN(fc_size);

	/* 所有（非xlog-switch）记录应包含数据。 */
	Assert(fc_size > SizeOfXLogRecord);

	/*
	 * 通过最小化在持有锁时必须进行的计算，来最小化自旋锁需要保持的持续时间。
	 * 保留WAL的当前尖端保存在CurrBytePos中，作为一个仅计算WAL中“可用”字节的字节位置，即，排除所有WAL页面头。
	 * “可用”字节位置与物理位置（XLogRecPtrs）之间的映射可以在未锁定区域外进行，
	 * 并且由于可用字节位置不包含任何头，因此从WAL保留X字节几乎和“CurrBytePos += X”一样简单。
	 */
	SpinLockAcquire(&fc_Insert->insertpos_lck);

	fc_startbytepos = fc_Insert->CurrBytePos;
	fc_endbytepos = fc_startbytepos + fc_size;
	fc_prevbytepos = fc_Insert->PrevBytePos;
	fc_Insert->CurrBytePos = fc_endbytepos;
	fc_Insert->PrevBytePos = fc_startbytepos;

	SpinLockRelease(&fc_Insert->insertpos_lck);

	*fc_StartPos = fc_XLogBytePosToRecPtr(fc_startbytepos);
	*fc_EndPos = fc_XLogBytePosToEndRecPtr(fc_endbytepos);
	*fc_PrevPtr = fc_XLogBytePosToRecPtr(fc_prevbytepos);

	/*
	 * 检查“可用字节位置”和XLogRecPtrs之间的转换在两个方向上的一致性。
	 */
	Assert(fc_XLogRecPtrToBytePos(*fc_StartPos) == fc_startbytepos);
	Assert(fc_XLogRecPtrToBytePos(*fc_EndPos) == fc_endbytepos);
	Assert(fc_XLogRecPtrToBytePos(*fc_PrevPtr) == fc_prevbytepos);
}

/*
 * 像 ReserveXLogInsertLocation()，但用于 xlog-switch 记录。
 *
 * xlog-switch 记录的处理方式略有不同。其余部分
 * 段将被保留用于此插入，如返回的
 * *EndPos 值所示。然而，如果我们已经在当前
 * 段的开头，则 *StartPos 和 *EndPos 设置为
 * 当前的位置，而不保留任何空间，函数返回 false。
*/
static bool fc_ReserveXLogSwitch(XLogRecPtr *fc_StartPos, XLogRecPtr *fc_EndPos, XLogRecPtr *fc_PrevPtr)
{
	XLogCtlInsert *fc_Insert = &XLogCtl->Insert;
	uint64		fc_startbytepos;
	uint64		fc_endbytepos;
	uint64		fc_prevbytepos;
	uint32		fc_size = MAXALIGN(SizeOfXLogRecord);
	XLogRecPtr	fc_ptr;
	uint32		fc_segleft;

	/*
	 * 这些计算在持有自旋锁时是有点负担重的，但由于我们保持着所有的 WAL 插入锁，
	 * 您没有其他插入者与之竞争。GetXLogInsertRecPtr() 确实
	 * 与其竞争，但这不是很频繁地调用。
	 */
	SpinLockAcquire(&fc_Insert->insertpos_lck);

	fc_startbytepos = fc_Insert->CurrBytePos;

	fc_ptr = fc_XLogBytePosToEndRecPtr(fc_startbytepos);
	if (XLogSegmentOffset(fc_ptr, wal_segment_size) == 0)
	{
		SpinLockRelease(&fc_Insert->insertpos_lck);
		*fc_EndPos = *fc_StartPos = fc_ptr;
		return false;
	}

	fc_endbytepos = fc_startbytepos + fc_size;
	fc_prevbytepos = fc_Insert->PrevBytePos;

	*fc_StartPos = fc_XLogBytePosToRecPtr(fc_startbytepos);
	*fc_EndPos = fc_XLogBytePosToEndRecPtr(fc_endbytepos);

	fc_segleft = wal_segment_size - XLogSegmentOffset(*fc_EndPos, wal_segment_size);
	if (fc_segleft != wal_segment_size)
	{
		/* 消耗该段的其余部分 */
		*fc_EndPos += fc_segleft;
		fc_endbytepos = fc_XLogRecPtrToBytePos(*fc_EndPos);
	}
	fc_Insert->CurrBytePos = fc_endbytepos;
	fc_Insert->PrevBytePos = fc_startbytepos;

	SpinLockRelease(&fc_Insert->insertpos_lck);

	*fc_PrevPtr = fc_XLogBytePosToRecPtr(fc_prevbytepos);

	Assert(XLogSegmentOffset(*fc_EndPos, wal_segment_size) == 0);
	Assert(fc_XLogRecPtrToBytePos(*fc_EndPos) == fc_endbytepos);
	Assert(fc_XLogRecPtrToBytePos(*fc_StartPos) == fc_startbytepos);
	Assert(fc_XLogRecPtrToBytePos(*fc_PrevPtr) == fc_prevbytepos);

	return true;
}

/*
 * XLogInsertRecord 的子例程。将 WAL 记录复制到已保留的
 * WAL 区域中。
 */
static void fc_CopyXLogRecordToWAL(int fc_write_len, bool fc_isLogSwitch, XLogRecData *fc_rdata,
					XLogRecPtr fc_StartPos, XLogRecPtr fc_EndPos, TimeLineID fc_tli)
{
	char	   *fc_currpos;
	int			fc_freespace;
	int			fc_written;
	XLogRecPtr	fc_CurrPos;
	XLogPageHeader fc_pagehdr;

	/*
	 * 获取指向正确 WAL 缓冲区中插入起始位置的指针。
	 */
	fc_CurrPos = fc_StartPos;
	fc_currpos = fc_GetXLogBuffer(fc_CurrPos, fc_tli);
	fc_freespace = INSERT_FREESPACE(fc_CurrPos);

	/*
	 * 此页面应至少有足够的空间用于第一个字段 (xl_tot_len)。
	 */
	Assert(fc_freespace >= sizeof(uint32));

	/* 复制记录数据 */
	fc_written = 0;
	while (fc_rdata != NULL)
	{
		char	   *fc_rdata_data = fc_rdata->data;
		int			fc_rdata_len = fc_rdata->len;

		while (fc_rdata_len > fc_freespace)
		{
			/*
			 * 写入符合该页面的内容，并继续在下一页面上进行。
			 */
			Assert(fc_CurrPos % XLOG_BLCKSZ >= SizeOfXLogShortPHD || fc_freespace == 0);
			memcpy(fc_currpos, fc_rdata_data, fc_freespace);
			fc_rdata_data += fc_freespace;
			fc_rdata_len -= fc_freespace;
			fc_written += fc_freespace;
			fc_CurrPos += fc_freespace;

			/*
			 * 获取指向下一页面开头的指针，并在页面头中设置 xlp_rem_len。
			 * 设置 XLP_FIRST_IS_CONTRECORD。
			 *
			 * 在页面上设置 contrecord 标志和 xlp_rem_len 是安全的，
			 * 无需持有锁。所有其他标志在页面初始化时已经设置，
			 * 在 AdvanceXLInsertBuffer 中，我们是唯一需要设置
			 * contrecord 标志的后端。
			 */
			fc_currpos = fc_GetXLogBuffer(fc_CurrPos, fc_tli);
			fc_pagehdr = (XLogPageHeader) fc_currpos;
			fc_pagehdr->xlp_rem_len = fc_write_len - fc_written;
			fc_pagehdr->xlp_info |= XLP_FIRST_IS_CONTRECORD;

			/* 跳过页面头 */
			if (XLogSegmentOffset(fc_CurrPos, wal_segment_size) == 0)
			{
				fc_CurrPos += SizeOfXLogLongPHD;
				fc_currpos += SizeOfXLogLongPHD;
			}
			else
			{
				fc_CurrPos += SizeOfXLogShortPHD;
				fc_currpos += SizeOfXLogShortPHD;
			}
			fc_freespace = INSERT_FREESPACE(fc_CurrPos);
		}

		Assert(fc_CurrPos % XLOG_BLCKSZ >= SizeOfXLogShortPHD || fc_rdata_len == 0);
		memcpy(fc_currpos, fc_rdata_data, fc_rdata_len);
		fc_currpos += fc_rdata_len;
		fc_CurrPos += fc_rdata_len;
		fc_freespace -= fc_rdata_len;
		fc_written += fc_rdata_len;

		fc_rdata = fc_rdata->next;
	}
	Assert(fc_written == fc_write_len);

	/*
	 * 如果这是一个 xlog-switch，仅仅写入切换记录是不够的，
	 * 我们还必须消耗 WAL 段中所有剩余空间。
	 * 我们已经保留了那个空间，但我们需要实际填充它。
	 */
	if (fc_isLogSwitch && XLogSegmentOffset(fc_CurrPos, wal_segment_size) != 0)
	{
		/* xlog-switch 记录除了头部不包含任何数据 */
		Assert(fc_write_len == SizeOfXLogRecord);

		/* 断言我们确实保留了正确的空间量 */
		Assert(XLogSegmentOffset(fc_EndPos, wal_segment_size) == 0);

		/* 用光当前页面上的所有剩余空间 */
		fc_CurrPos += fc_freespace;

		/*
		 * 导致段中的所有剩余页面被刷新，使 XLog 位置
		 * 保持在应该的位置，即下一段的开始。
		 * 我们一次处理一页，以确保我们不会在 wal_buffers < wal_segment_size 的情况下
		 * 与自己死锁。
		 */
		while (fc_CurrPos < fc_EndPos)
		{
			/*
			 * 刷新页面的最小操作是调用
			 * WALInsertLockUpdateInsertingAt(CurrPos)，然后调用
			 * AdvanceXLInsertBuffer(...)。页面将主要被初始化为零，
			 * 除了页面头（始终是短变体，因为这永远不是段的第一页）。
			 *
			 * 大量的零有利于压缩，但每隔 XLOG_BLCKSZ 的头部（其值在页之间不同）
			 * 则不是。效果因压缩工具而异，但例如 bzip2 如果保留这些头部，压缩效果会差一个数量级。
			 *
			 * 与其在 AdvanceXLInsertBuffer 中复杂化（在重负载情况下也会调用此函数），
			 * 不如使用 GetXLogBuffer（它本身调用我们需要的两个方法）来获取指针并将大部分页面归零。
			 * 然后我们只需将页面头部归零。
			 */
			fc_currpos = fc_GetXLogBuffer(fc_CurrPos, fc_tli);
			MemSet(fc_currpos, 0, SizeOfXLogShortPHD);

			fc_CurrPos += XLOG_BLCKSZ;
		}
	}
	else
	{
		/* 对结束位置进行对齐，以便下一个记录以对齐方式开始 */
		fc_CurrPos = MAXALIGN64(fc_CurrPos);
	}

	if (fc_CurrPos != fc_EndPos)
		elog(PANIC, "space reserved for WAL record does not match what was written");
}

/*
 * 获取 WAL 插入锁，以便插入到 WAL。
 */
static void fc_WALInsertLockAcquire(void)
{
	bool		fc_immed;

	/*
	 * 我们获取哪个 WAL 插入锁并不重要，因此尝试上次使用的那个。
	 * 如果系统不是特别繁忙，它仍然可用的可能性很大，
	 * 并且与特定锁有一定亲和性是好的，这样可以避免在没有竞争时不必要地在进程之间切换缓存行。
	 *
	 * 如果这是该后端的第一次运行，随机选择一个锁。
	 * 这样可以使锁在非常多的短连接中被均匀使用。
	 */
	static int	fc_lockToTry = -1;

	if (fc_lockToTry == -1)
		fc_lockToTry = MyProc->pgprocno % NUM_XLOGINSERT_LOCKS;
	MyLockNo = fc_lockToTry;

	/*
	 * insertingAt 值最初设置为 0，因为我们还不知道我们的插入位置。
	 */
	fc_immed = LWLockAcquire(&WALInsertLocks[MyLockNo].l.lock, LW_EXCLUSIVE);
	if (!fc_immed)
	{
		/*
		 * 如果我们无法立即获取锁，下次尝试另一个锁。
		 * 在插入锁比并发插入者多的系统上，这会导致所有插入者最终迁移到没有其他人使用的锁上。
		 * 在插入者比锁多的系统上，它仍有助于在锁之间均匀分配插入者。
		 */
		fc_lockToTry = (fc_lockToTry + 1) % NUM_XLOGINSERT_LOCKS;
	}
}

/*
 * 获取所有 WAL 插入锁，以防止其他后端插入到 WAL。
 */
static void fc_WALInsertLockAcquireExclusive(void)
{
	int			fc_i;

	/*
	 * 在持有所有锁时，除了最后一个锁的 insertingAt 指示符外，
	 * 其他的都设置为 0xFFFFFFFFFFFFFFFF，这高于任何真实的 XLogRecPtr 值，
	 * 以确保没有人会因等待这些而阻塞。
	 */
	for (fc_i = 0; fc_i < NUM_XLOGINSERT_LOCKS - 1; fc_i++)
	{
		LWLockAcquire(&WALInsertLocks[fc_i].l.lock, LW_EXCLUSIVE);
		LWLockUpdateVar(&WALInsertLocks[fc_i].l.lock,
						&WALInsertLocks[fc_i].l.insertingAt,
						PG_UINT64_MAX);
	}
	/* 变量值在释放时重置为 0 */
	LWLockAcquire(&WALInsertLocks[fc_i].l.lock, LW_EXCLUSIVE);

	holdingAllLocks = true;
}

/*
 * 释放我们的插入锁（或锁，如果我们持有所有锁）。
 *
 * 注意：将所有变量重置为 0，这样在下次获取锁时，LWLockWaitForVar 会阻塞。
 */
static void fc_WALInsertLockRelease(void)
{
	if (holdingAllLocks)
	{
		int			fc_i;

		for (fc_i = 0; fc_i < NUM_XLOGINSERT_LOCKS; fc_i++)
			LWLockReleaseClearVar(&WALInsertLocks[fc_i].l.lock,
								  &WALInsertLocks[fc_i].l.insertingAt,
								  0);

		holdingAllLocks = false;
	}
	else
	{
		LWLockReleaseClearVar(&WALInsertLocks[MyLockNo].l.lock,
							  &WALInsertLocks[MyLockNo].l.insertingAt,
							  0);
	}
}

/*
 * 更新我们的 insertingAt 值，以让其他人知道我们已完成
 * 插入到这一点为止。
 */
static void fc_WALInsertLockUpdateInsertingAt(XLogRecPtr fc_insertingAt)
{
	if (holdingAllLocks)
	{
		/*
		 * 我们使用最后一个锁来标记我们的实际位置，参见
		 * WALInsertLockAcquireExclusive中的注释。
		 */
		LWLockUpdateVar(&WALInsertLocks[NUM_XLOGINSERT_LOCKS - 1].l.lock,
						&WALInsertLocks[NUM_XLOGINSERT_LOCKS - 1].l.insertingAt,
						fc_insertingAt);
	}
	else
		LWLockUpdateVar(&WALInsertLocks[MyLockNo].l.lock,
						&WALInsertLocks[MyLockNo].l.insertingAt,
						fc_insertingAt);
}

/*
 * 等待任何WAL插入 < upto 完成。
 *
 * 返回仍在进行中的最旧插入的位置。
 * 在此之前的任何WAL都已完全复制到WAL缓存中，并且
 * 可以刷新到磁盘。因为这等待比'upto'老的任何插入
 * 完成，所以返回值总是 >= 'upto'。
 *
 * 注意：当你即将写出WAL时，必须在获取WALWriteLock之前调用此函数，
 * 以避免死锁。这个函数可能需要等待一个插入完成（或至少推进到下一个
 * 未初始化的页面），而插入者可能需要逐出一个旧的WAL缓存
 * 来为一个新的插入腾出空间，这又需要WALWriteLock。
 */
static XLogRecPtr fc_WaitXLogInsertionsToFinish(XLogRecPtr fc_upto)
{
	uint64		fc_bytepos;
	XLogRecPtr	fc_reservedUpto;
	XLogRecPtr	fc_finishedUpto;
	XLogCtlInsert *fc_Insert = &XLogCtl->Insert;
	int			fc_i;

	if (MyProc == NULL)
		elog(PANIC, "cannot wait without a PGPROC structure");

	/* 读取当前插入位置 */
	SpinLockAcquire(&fc_Insert->insertpos_lck);
	fc_bytepos = fc_Insert->CurrBytePos;
	SpinLockRelease(&fc_Insert->insertpos_lck);
	fc_reservedUpto = fc_XLogBytePosToEndRecPtr(fc_bytepos);

	/*
	 * 没有人应该请求刷新一段甚至尚未保留的WAL。
	 * 但是，如果磁盘上有一个虚假的LSN的块，这种情况仍可能发生。
	 * XLogFlush会检查这种情况并发出警告，但仅在刷新之后。
	 * 在这里，我们只是假设这意味着所有已保留的WAL都需要完成。
	 * 在这一特殊情况下，返回值可能小于'upto'参数。
	 */
	if (fc_upto > fc_reservedUpto)
	{
		ereport(LOG,
				(errmsg("request to flush past end of generated WAL; request %X/%X, current position %X/%X",
						LSN_FORMAT_ARGS(fc_upto), LSN_FORMAT_ARGS(fc_reservedUpto))));
		fc_upto = fc_reservedUpto;
	}

	/*
	 * 循环遍历所有锁，等待任何比'upto'更早的进行中的插入。
	 *
	 * finishedUpto是我们的返回值，指示所有WAL插入已完成的点。
	 * 将其初始化为已保留WAL的头部，并在遍历插入锁时，
	 * 对于任何仍在进行的插入将其向后退。
	 */
	fc_finishedUpto = fc_reservedUpto;
	for (fc_i = 0; fc_i < NUM_XLOGINSERT_LOCKS; fc_i++)
	{
		XLogRecPtr	fc_insertingat = InvalidXLogRecPtr;

		do
		{
			/*
			 * 查看这个插入是否正在进行。 LWLockWaitForVar将
			 * 等待锁被释放，或等待LWLockUpdateVar调用设置'value'。
			 * 当锁首次被获取时，其值为0（InvalidXLogRecPtr），
			 * 这意味着我们还不知道它正在插入哪里。
			 * 我们将不得不等待。如果这是一个小插入，记录很可能
			 * 会适合同一页面，插入者将释放锁而不调用LWLockUpdateVar。
			 * 但是如果它必须等待，它将在睡眠之前通过LWLockUpdateVar
			 * 通告插入点。
			 */
			if (LWLockWaitForVar(&WALInsertLocks[fc_i].l.lock,
								 &WALInsertLocks[fc_i].l.insertingAt,
								 fc_insertingat, &fc_insertingat))
			{
				/* 锁是空闲的，因此没有进行中的插入 */
				fc_insertingat = InvalidXLogRecPtr;
				break;
			}

			/*
			 * 这个插入仍在进行中。必须等待，除非
			 * 插入者已经超过了'upto'。
			 */
		} while (fc_insertingat < fc_upto);

		if (fc_insertingat != InvalidXLogRecPtr && fc_insertingat < fc_finishedUpto)
			fc_finishedUpto = fc_insertingat;
	}
	return fc_finishedUpto;
}

/*
 * 获取指向WAL缓冲区中包含给定XLogRecPtr的正确位置的指针。
 *
 * 如果页面尚未初始化，则进行初始化。那可能需要从缓冲区缓存中驱逐一个旧的脏缓冲区，这意味着I/O。
 *
 * 调用者必须确保包含请求位置的页面尚未被驱逐，且不会被驱逐。确保这一点的方法是持有一个WAL插入锁，并将insertingAt位置设置为小于等于ptr的某个值。如果需要从缓冲区驱逐一个旧页面，GetXLogBuffer()将更新insertingAt。（这意味着，一旦您使用给定的'ptr'调用GetXLogBuffer()，则不得再访问早于该点的任何内容，也不得迟后调用GetXLogBuffer()，传入较旧的'ptr'，因为旧的缓冲区可能已经被回收）
*/
static char * fc_GetXLogBuffer(XLogRecPtr fc_ptr, TimeLineID fc_tli)
{
	int			fc_idx;
	XLogRecPtr	fc_endptr;
	static uint64 fc_cachedPage = 0;
	static char *fc_cachedPos = NULL;
	XLogRecPtr	fc_expectedEndPtr;

	/*
	 * 针对我们需要再次访问与上次相同页面的常见情况的快速路径。
	 */
	if (fc_ptr / XLOG_BLCKSZ == fc_cachedPage)
	{
		Assert(((XLogPageHeader) fc_cachedPos)->xlp_magic == XLOG_PAGE_MAGIC);
		Assert(((XLogPageHeader) fc_cachedPos)->xlp_pageaddr == fc_ptr - (fc_ptr % XLOG_BLCKSZ));
		return fc_cachedPos + fc_ptr % XLOG_BLCKSZ;
	}

	/*
	 * XLog缓冲区缓存的组织方式是，页面总是加载到特定的缓冲区中。这样，我们可以仅从XLogRecPtr轻松计算出给定页面必须加载到的缓冲区。
	 */
	fc_idx = XLogRecPtrToBufIdx(fc_ptr);

	/*
	 * 查看当前缓冲区中加载的是哪个页面。它可能是我们正在寻找的页面，或者是某个较旧的页面。它不能是更新的页面 - 这将意味着我们正在寻找的页面已经被写入磁盘并且被驱逐，调用者有责任确保这种情况不会发生。
	 *
	 * 然而，我们在读取值时不持锁。如果有人刚刚初始化了页面，则在该平台上，如果64位获取不是原子的，我们获取XLogRecPtr时可能会出现"撕裂读取"。在这种情况下，我们将看到一个虚假的值。没关系，如果我们看到与我们正在寻找的页面不同的任何值，我们将获取映射锁（在AdvanceXLInsertBuffer中）并重试。但是，这意味着当我们执行此未锁定读取时，我们可能会看到一个看似超前于我们正在寻找的页面的值。对此不要惊慌，直到我们在持有锁的情况下验证该值。
	 */
	fc_expectedEndPtr = fc_ptr;
	fc_expectedEndPtr += XLOG_BLCKSZ - fc_ptr % XLOG_BLCKSZ;

	fc_endptr = XLogCtl->xlblocks[fc_idx];
	if (fc_expectedEndPtr != fc_endptr)
	{
		XLogRecPtr	fc_initializedUpto;

		/*
		 * 在调用AdvanceXLInsertBuffer()之前，它可能会阻塞，让其他人知道我们在插入记录时完成的进度。
		 *
		 * 注意：如果'ptr'指向页面头之后的位置，请在页面的开头处报告一个位置，而不是'ptr'本身。如果没有其他插入正在进行，可能会有人试图刷新到我们所报告的位置。如果我们报告了页面头之后的位置，则可能会有人试图刷新页面头，即使页面实际上可能尚未初始化。作为页面上的第一个插入者，我们实际上负责确保在让insertingAt移动过页面头之前先初始化页面。
		 */
		if (fc_ptr % XLOG_BLCKSZ == SizeOfXLogShortPHD &&
			XLogSegmentOffset(fc_ptr, wal_segment_size) > XLOG_BLCKSZ)
			fc_initializedUpto = fc_ptr - SizeOfXLogShortPHD;
		else if (fc_ptr % XLOG_BLCKSZ == SizeOfXLogLongPHD &&
				 XLogSegmentOffset(fc_ptr, wal_segment_size) < XLOG_BLCKSZ)
			fc_initializedUpto = fc_ptr - SizeOfXLogLongPHD;
		else
			fc_initializedUpto = fc_ptr;

		fc_WALInsertLockUpdateInsertingAt(fc_initializedUpto);

		fc_AdvanceXLInsertBuffer(fc_ptr, fc_tli, false);
		fc_endptr = XLogCtl->xlblocks[fc_idx];

		if (fc_expectedEndPtr != fc_endptr)
			elog(PANIC, "could not find WAL buffer for %X/%X",
				 LSN_FORMAT_ARGS(fc_ptr));
	}
	else
	{
		/*
		 * 确保页面的初始化对我们是可见的，并且
		 * 不会在稍后到达覆盖我们在页面上写入的WAL数据。
		 */
		pg_memory_barrier();
	}

	/*
	 * 找到持有此页面的缓冲区。返回页面内正确的
	 * 偏移量的指针。
	 */
	fc_cachedPage = fc_ptr / XLOG_BLCKSZ;
	fc_cachedPos = XLogCtl->pages + fc_idx * (Size) XLOG_BLCKSZ;

	Assert(((XLogPageHeader) fc_cachedPos)->xlp_magic == XLOG_PAGE_MAGIC);
	Assert(((XLogPageHeader) fc_cachedPos)->xlp_pageaddr == fc_ptr - (fc_ptr % XLOG_BLCKSZ));

	return fc_cachedPos + fc_ptr % XLOG_BLCKSZ;
}

/*
 * 将“可用字节位置”转换为XLogRecPtr。可用字节位置
 * 是从WAL开始的，排除所有WAL
 * 页面头部。
 */
static XLogRecPtr fc_XLogBytePosToRecPtr(uint64 fc_bytepos)
{
	uint64		fc_fullsegs;
	uint64		fc_fullpages;
	uint64		fc_bytesleft;
	uint32		fc_seg_offset;
	XLogRecPtr	fc_result;

	fc_fullsegs = fc_bytepos / UsableBytesInSegment;
	fc_bytesleft = fc_bytepos % UsableBytesInSegment;

	if (fc_bytesleft < XLOG_BLCKSZ - SizeOfXLogLongPHD)
	{
		/* 适合在段的第一页上 */
		fc_seg_offset = fc_bytesleft + SizeOfXLogLongPHD;
	}
	else
	{
		/* 考虑到长头部的段的第一页 */
		fc_seg_offset = XLOG_BLCKSZ;
		fc_bytesleft -= XLOG_BLCKSZ - SizeOfXLogLongPHD;

		fc_fullpages = fc_bytesleft / UsableBytesInPage;
		fc_bytesleft = fc_bytesleft % UsableBytesInPage;

		fc_seg_offset += fc_fullpages * XLOG_BLCKSZ + fc_bytesleft + SizeOfXLogShortPHD;
	}

	XLogSegNoOffsetToRecPtr(fc_fullsegs, fc_seg_offset, wal_segment_size, fc_result);

	return fc_result;
}

/*
 * 类似于XLogBytePosToRecPtr，但如果位置处于页面边界，
 * 返回指向页面开头的指针（即，页面头部之前），
 * 而不是指向该页面上第一条xlog记录应该去的地方。用于
 * 将指针转换为记录的结尾。
 */
static XLogRecPtr fc_XLogBytePosToEndRecPtr(uint64 fc_bytepos)
{
	uint64		fc_fullsegs;
	uint64		fc_fullpages;
	uint64		fc_bytesleft;
	uint32		fc_seg_offset;
	XLogRecPtr	fc_result;

	fc_fullsegs = fc_bytepos / UsableBytesInSegment;
	fc_bytesleft = fc_bytepos % UsableBytesInSegment;

	if (fc_bytesleft < XLOG_BLCKSZ - SizeOfXLogLongPHD)
	{
		/* 适合在段的第一页上 */
		if (fc_bytesleft == 0)
			fc_seg_offset = 0;
		else
			fc_seg_offset = fc_bytesleft + SizeOfXLogLongPHD;
	}
	else
	{
		/* 考虑到长头部的段的第一页 */
		fc_seg_offset = XLOG_BLCKSZ;
		fc_bytesleft -= XLOG_BLCKSZ - SizeOfXLogLongPHD;

		fc_fullpages = fc_bytesleft / UsableBytesInPage;
		fc_bytesleft = fc_bytesleft % UsableBytesInPage;

		if (fc_bytesleft == 0)
			fc_seg_offset += fc_fullpages * XLOG_BLCKSZ + fc_bytesleft;
		else
			fc_seg_offset += fc_fullpages * XLOG_BLCKSZ + fc_bytesleft + SizeOfXLogShortPHD;
	}

	XLogSegNoOffsetToRecPtr(fc_fullsegs, fc_seg_offset, wal_segment_size, fc_result);

	return fc_result;
}

/*
 * 将XLogRecPtr转换为“可用字节位置”。
 */
static uint64 fc_XLogRecPtrToBytePos(XLogRecPtr fc_ptr)
{
	uint64		fc_fullsegs;
	uint32		fc_fullpages;
	uint32		fc_offset;
	uint64		fc_result;

	XLByteToSeg(fc_ptr, fc_fullsegs, wal_segment_size);

	fc_fullpages = (XLogSegmentOffset(fc_ptr, wal_segment_size)) / XLOG_BLCKSZ;
	fc_offset = fc_ptr % XLOG_BLCKSZ;

	if (fc_fullpages == 0)
	{
		fc_result = fc_fullsegs * UsableBytesInSegment;
		if (fc_offset > 0)
		{
			Assert(fc_offset >= SizeOfXLogLongPHD);
			fc_result += fc_offset - SizeOfXLogLongPHD;
		}
	}
	else
	{
		fc_result = fc_fullsegs * UsableBytesInSegment +
			(XLOG_BLCKSZ - SizeOfXLogLongPHD) + /* 考虑到第一页 */
			(fc_fullpages - 1) * UsableBytesInPage;	/* 完整页面 */
		if (fc_offset > 0)
		{
			Assert(fc_offset >= SizeOfXLogShortPHD);
			fc_result += fc_offset - SizeOfXLogShortPHD;
		}
	}

	return fc_result;
}

/*
 * 初始化XLOG缓冲区，写出仍然包含
 * 未写入数据的旧缓冲区，直到包含‘upto’的页面。或者如果'opportunistic'为
 * true，则初始化尽可能多的页面而不必写出
 * 未写入的数据。任何新页面都初始化为零，并且页面头部
 * 被正确初始化。
 */
static void fc_AdvanceXLInsertBuffer(XLogRecPtr fc_upto, TimeLineID fc_tli, bool fc_opportunistic)
{
	XLogCtlInsert *fc_Insert = &XLogCtl->Insert;
	int			fc_nextidx;
	XLogRecPtr	fc_OldPageRqstPtr;
	XLogwrtRqst fc_WriteRqst;
	XLogRecPtr	fc_NewPageEndPtr = InvalidXLogRecPtr;
	XLogRecPtr	fc_NewPageBeginPtr;
	XLogPageHeader fc_NewPage;
	int			fc_npages pg_attribute_unused() = 0;

	LWLockAcquire(WALBufMappingLock, LW_EXCLUSIVE);

	/*
	 * 现在我们已经获得了锁，请检查是否有人已经初始化了页面。
	 */
	while (fc_upto >= XLogCtl->InitializedUpTo || fc_opportunistic)
	{
		fc_nextidx = XLogRecPtrToBufIdx(XLogCtl->InitializedUpTo);

		/*
		 * 获取需要替换的缓冲页面的结束偏移量（如果缓冲区尚未使用，
		 * 则可能为零）。如果已经写出，则继续。
		 */
		fc_OldPageRqstPtr = XLogCtl->xlblocks[fc_nextidx];
		if (LogwrtResult.Write < fc_OldPageRqstPtr)
		{
			/*
			 * 不，工作还没完成。如果我们只想在不刷新
			 * 的情况下进行预初始化，现在放弃。
			 */
			if (fc_opportunistic)
				break;

			/* 在等待之前，获取info_lck并更新LogwrtResult */
			SpinLockAcquire(&XLogCtl->info_lck);
			if (XLogCtl->LogwrtRqst.Write < fc_OldPageRqstPtr)
				XLogCtl->LogwrtRqst.Write = fc_OldPageRqstPtr;
			LogwrtResult = XLogCtl->LogwrtResult;
			SpinLockRelease(&XLogCtl->info_lck);

			/*
			 * 现在我们有了最新的LogwrtResult值，查看我们
			 * 是否仍需要写入，或是否已经有人写入。
			 */
			if (LogwrtResult.Write < fc_OldPageRqstPtr)
			{
				/*
				 * 必须获取写锁。先释放WALBufMappingLock，
				 * 以确保我们需要等待的所有插入
				 * 可以完成（最多到这个位置）。否则我们会冒
				 * 死锁的风险。
				 */
				LWLockRelease(WALBufMappingLock);

				fc_WaitXLogInsertionsToFinish(fc_OldPageRqstPtr);

				LWLockAcquire(WALWriteLock, LW_EXCLUSIVE);

				LogwrtResult = XLogCtl->LogwrtResult;
				if (LogwrtResult.Write >= fc_OldPageRqstPtr)
				{
					/* 好的，有人已经写入了 */
					LWLockRelease(WALWriteLock);
				}
				else
				{
					/* 必须自己写入 */
					TRACE_POSTGRESQL_WAL_BUFFER_WRITE_DIRTY_START();
					fc_WriteRqst.Write = fc_OldPageRqstPtr;
					fc_WriteRqst.Flush = 0;
					fc_XLogWrite(fc_WriteRqst, fc_tli, false);
					LWLockRelease(WALWriteLock);
					PendingWalStats.wal_buffers_full++;
					TRACE_POSTGRESQL_WAL_BUFFER_WRITE_DIRTY_DONE();
				}
				/* 重新获取WALBufMappingLock并重试 */
				LWLockAcquire(WALBufMappingLock, LW_EXCLUSIVE);
				continue;
			}
		}

		/*
		 * 现在下一个缓冲槽是空闲的，我们可以将其设置为
		 * 下一个输出页面。
		 */
		fc_NewPageBeginPtr = XLogCtl->InitializedUpTo;
		fc_NewPageEndPtr = fc_NewPageBeginPtr + XLOG_BLCKSZ;

		Assert(XLogRecPtrToBufIdx(fc_NewPageBeginPtr) == fc_nextidx);

		fc_NewPage = (XLogPageHeader) (XLogCtl->pages + fc_nextidx * (Size) XLOG_BLCKSZ);

		/*
		 * 确保重新归零缓冲区，以便我们已
		 * 写入的字节之后的内容看起来像零而不是有效的XLOG记录...
		 */
		MemSet((char *) fc_NewPage, 0, XLOG_BLCKSZ);

		/*
		 * 填充新页面的头部
		 */
		fc_NewPage->xlp_magic = XLOG_PAGE_MAGIC;

		/* NewPage->xlp_info = 0; */	/* 通过memset完成 */
		fc_NewPage->xlp_tli = fc_tli;
		fc_NewPage->xlp_pageaddr = fc_NewPageBeginPtr;

		/* NewPage->xlp_rem_len = 0; */	/* 通过memset完成 */

		/*
		 * 如果在线备份未在进行中，标记头以指示
		 * 从此页面开始的WAL记录具有可移除的备份
		 * 块。这使得WAL归档程序知道是否可以安全地
		 * 通过将完整块记录转换为非完整块格式来压缩存档的WAL数据。
		 * 在页面级别记录这一点是足够的，因为在开始备份时我们
		 * 强制进行页面切换（实际上是段切换），因此在备份期间
		 * 任何记录可以被写入之前标志将处于关闭状态。在备份结束时，
		 * 最后一页将被标记为全部不安全，虽然可能只有部分是不安全的，
		 * 但在最坏的情况下，归档程序会错过压缩一些记录的机会。
		 */
		if (!fc_Insert->forcePageWrites)
			fc_NewPage->xlp_info |= XLP_BKP_REMOVABLE;

		/*
		 * 如果是XLOG段文件的第一页，设置为长头。
		 */
		if ((XLogSegmentOffset(fc_NewPage->xlp_pageaddr, wal_segment_size)) == 0)
		{
			XLogLongPageHeader fc_NewLongPage = (XLogLongPageHeader) fc_NewPage;

			fc_NewLongPage->xlp_sysid = ControlFile->system_identifier;
			fc_NewLongPage->xlp_seg_size = wal_segment_size;
			fc_NewLongPage->xlp_xlog_blcksz = XLOG_BLCKSZ;
			fc_NewPage->xlp_info |= XLP_LONG_HEADER;
		}

		/*
		 * 确保页面的初始化在xlblocks更新之前对其他人可见。
		 * GetXLogBuffer() 在不持有锁的情况下读取xlblocks。
		 */
		pg_write_barrier();

		*((volatile XLogRecPtr *) &XLogCtl->xlblocks[fc_nextidx]) = fc_NewPageEndPtr;

		XLogCtl->InitializedUpTo = fc_NewPageEndPtr;

		fc_npages++;
	}
	LWLockRelease(WALBufMappingLock);

#ifdef WAL_DEBUG
	if (XLOG_DEBUG && fc_npages > 0)
	{
		elog(DEBUG1, "initialized %d pages, up to %X/%X",
			 fc_npages, LSN_FORMAT_ARGS(fc_NewPageEndPtr));
	}
#endif
}

/*
 * 根据max_wal_size_mb和checkpoint_completion_target
 * 计算CheckPointSegments。
 */
static void fc_CalculateCheckpointSegments(void)
{
	double		fc_target;

	/*-------
	 * 计算触发检查点的距离，以避免超过max_wal_size_mb。
	 * 这基于两个假设：
	 *
	 * a) 我们仅保留一个检查点周期的WAL（在PG11之前，我们保留了
	 *    两个检查点周期的WAL，以便在第一个检查点失败时从
	 *    第二个检查点恢复，尽管这仅在主节点上进行，而
	 *    不在备用节点上。仅保留一个检查点简化了处理并减少了
	 *    许多较小数据库中的磁盘空间。）
	 * b) 在检查点期间，我们消耗checkpoint_completion_target *
	 *	  检查点之间消耗的段数量。
	 *-------
	 */
	fc_target = (double) ConvertToXSegs(max_wal_size_mb, wal_segment_size) /
		(1.0 + CheckPointCompletionTarget);

	/* 向下取整 */
	CheckPointSegments = (int) fc_target;

	if (CheckPointSegments < 1)
		CheckPointSegments = 1;
}

void assign_max_wal_size(int fc_newval, void *fc_extra)
{
	max_wal_size_mb = fc_newval;
	fc_CalculateCheckpointSegments();
}

void assign_checkpoint_completion_target(double fc_newval, void *fc_extra)
{
	CheckPointCompletionTarget = fc_newval;
	fc_CalculateCheckpointSegments();
}

/*
 * 在检查点时，多少个WAL段被回收作为预分配的未来
 * XLOG段？返回应预分配的最高段。
 */
static XLogSegNo
fc_XLOGfileslop(XLogRecPtr fc_lastredoptr)
{
	XLogSegNo	fc_minSegNo;
	XLogSegNo	fc_maxSegNo;
	double		fc_distance;
	XLogSegNo	fc_recycleSegNo;

	/*
	 * 计算min_wal_size_mb和max_wal_size_mb对应的段编号。
	 * 始终回收足够的段以满足最小值，并移除足够的段以保持在最大值以下。
	 */
	fc_minSegNo = fc_lastredoptr / wal_segment_size +
		ConvertToXSegs(min_wal_size_mb, wal_segment_size) - 1;
	fc_maxSegNo = fc_lastredoptr / wal_segment_size +
		ConvertToXSegs(max_wal_size_mb, wal_segment_size) - 1;

	/*
	 * 在这些限制之间，回收足够的段以使我们能顺利通过到
	 * 下一个检查点的估计结束。
	 *
	 * 为了估计下一个检查点的完成位置，假设系统在每个检查点之间
	 * 稳定运行，消耗CheckPointDistanceEstimate字节。
	 */
	fc_distance = (1.0 + CheckPointCompletionTarget) * CheckPointDistanceEstimate;
	/* 加10%以确保安全。 */
	fc_distance *= 1.10;

	fc_recycleSegNo = (XLogSegNo) ceil(((double) fc_lastredoptr + fc_distance) /
									wal_segment_size);

	if (fc_recycleSegNo < fc_minSegNo)
		fc_recycleSegNo = fc_minSegNo;
	if (fc_recycleSegNo > fc_maxSegNo)
		fc_recycleSegNo = fc_maxSegNo;

	return fc_recycleSegNo;
}

/*
 * 检查我们是否消耗了足够的xlog空间以至于需要一个检查点。
 *
 * new_segno表示刚刚填满（或在恢复过程中读取）的日志文件。
 * 我们测量从RedoRecPtr到new_segno的距离，看看是否超过了
 * CheckPointSegments。
 *
 * 注意：确保RedoRecPtr是最新的责任在调用者。
 */
bool XLogCheckpointNeeded(XLogSegNo fc_new_segno)
{
	XLogSegNo	fc_old_segno;

	XLByteToSeg(RedoRecPtr, fc_old_segno, wal_segment_size);

	if (fc_new_segno >= fc_old_segno + (uint64) (CheckPointSegments - 1))
		return true;
	return false;
}

/*
 * 至少按照 WriteRqst 指示写入和/或 fsync 日志。
 *
 * 如果 flexible == true，我们不必写入到 WriteRqst 指示的程度，但
 * 可以在任何方便的边界（例如缓存或日志文件边界）停止。
 * 这个选项允许我们避免不必要地发出多个写入请求，而只用一个就可以了。
 *
 * 必须在持有 WALWriteLock 的情况下调用。必须在抓取锁之前调用
 * WaitXLogInsertionsToFinish(WriteRqst)，以确保数据已准备好写入。
 */
static void fc_XLogWrite(XLogwrtRqst fc_WriteRqst, TimeLineID fc_tli, bool fc_flexible)
{
	bool		fc_ispartialpage;
	bool		fc_last_iteration;
	bool		fc_finishing_seg;
	int			fc_curridx;
	int			fc_npages;
	int			fc_startidx;
	uint32		fc_startoffset;

	/* 我们应该始终在临界区内 */
	Assert(CritSectionCount > 0);

	/*
	 * 更新本地 LogwrtResult（调用者可能已经这样做了，但...）
	 */
	LogwrtResult = XLogCtl->LogwrtResult;

	/*
	 * 由于 xlog 缓存中的连续页面是按顺序分配的，
	 * 我们通常可以将多个页面聚集在一起，仅发出一个
	 * write() 调用。npages 是我们确定可以一起写的页面数；
	 * startidx 是第一个页面的缓存块索引，
	 * startoffset 是它应该放置的文件偏移量。这两个
	 * 变量在 npages > 0 时才有效，但我们必须初始化
	 * 所有变量以保持编译器的安静。
	 */
	fc_npages = 0;
	fc_startidx = 0;
	fc_startoffset = 0;

	/*
	 * 在循环中，curridx 是要考虑写入的页面的缓存块索引。
	 * 从包含下一个未写页面或最后一个部分写入页面的缓冲区开始。
	 */
	fc_curridx = XLogRecPtrToBufIdx(LogwrtResult.Write);

	while (LogwrtResult.Write < fc_WriteRqst.Write)
	{
		/*
		 * 确保我们没有超前于插入过程。这可能发生在
		 * 我们收到一个虚假的 WriteRqst.Write，它超出了
		 * AdvanceXLInsertBuffer 初始化的最后一个页面的末尾。
		 */
		XLogRecPtr	fc_EndPtr = XLogCtl->xlblocks[fc_curridx];

		if (LogwrtResult.Write >= fc_EndPtr)
			elog(PANIC, "xlog write request %X/%X is past end of log %X/%X",
				 LSN_FORMAT_ARGS(LogwrtResult.Write),
				 LSN_FORMAT_ARGS(fc_EndPtr));

		/* 将 LogwrtResult.Write 推进到当前缓冲区页面的末尾 */
		LogwrtResult.Write = fc_EndPtr;
		fc_ispartialpage = fc_WriteRqst.Write < LogwrtResult.Write;

		if (!XLByteInPrevSeg(LogwrtResult.Write, openLogSegNo,
							 wal_segment_size))
		{
			/*
			 * 切换到新的日志文件段。我们在这里不能有任何待处理的
			 * 页面（因为我们在段末尾转储我们拥有的数据）。
			 */
			Assert(fc_npages == 0);
			if (openLogFile >= 0)
				fc_XLogFileClose();
			XLByteToPrevSeg(LogwrtResult.Write, openLogSegNo,
							wal_segment_size);
			openLogTLI = fc_tli;

			/* 创建/使用新的日志文件 */
			openLogFile = XLogFileInit(openLogSegNo, fc_tli);
			ReserveExternalFD();
		}

		/* 确保我们已打开当前日志文件 */
		if (openLogFile < 0)
		{
			XLByteToPrevSeg(LogwrtResult.Write, openLogSegNo,
							wal_segment_size);
			openLogTLI = fc_tli;
			openLogFile = XLogFileOpen(openLogSegNo, fc_tli);
			ReserveExternalFD();
		}

		/* 将当前页面添加到待转储页面集合中 */
		if (fc_npages == 0)
		{
			/* 组的第一个 */
			fc_startidx = fc_curridx;
			fc_startoffset = XLogSegmentOffset(LogwrtResult.Write - XLOG_BLCKSZ,
											wal_segment_size);
		}
		fc_npages++;

		/*
		 * 如果这是最后一次循环迭代，或者如果我们在缓存区的最后一页
		 * （因为下一页在内存中不会是连续的），或者如果我们在日志文件
		 * 段的末尾，则转储该集合。
		 */
		fc_last_iteration = fc_WriteRqst.Write <= LogwrtResult.Write;

		fc_finishing_seg = !fc_ispartialpage &&
			(fc_startoffset + fc_npages * XLOG_BLCKSZ) >= wal_segment_size;

		if (fc_last_iteration ||
			fc_curridx == XLogCtl->XLogCacheBlck ||
			fc_finishing_seg)
		{
			char	   *fc_from;
			Size		fc_nbytes;
			Size		fc_nleft;
			int			fc_written;
			instr_time	fc_start;

			/* 可以写入页面 */
			fc_from = XLogCtl->pages + fc_startidx * (Size) XLOG_BLCKSZ;
			fc_nbytes = fc_npages * (Size) XLOG_BLCKSZ;
			fc_nleft = fc_nbytes;
			do
			{
				errno = 0;

				/* 测量写入 WAL 数据的 I/O 时间 */
				if (track_wal_io_timing)
					INSTR_TIME_SET_CURRENT(fc_start);

				pgstat_report_wait_start(WAIT_EVENT_WAL_WRITE);
				fc_written = pg_pwrite(openLogFile, fc_from, fc_nleft, fc_startoffset);
				pgstat_report_wait_end();

				/*
				 * 增加 I/O 时间和 WAL 数据写入磁盘的次数。
				 */
				if (track_wal_io_timing)
				{
					instr_time	fc_duration;

					INSTR_TIME_SET_CURRENT(fc_duration);
					INSTR_TIME_SUBTRACT(fc_duration, fc_start);
					PendingWalStats.wal_write_time += INSTR_TIME_GET_MICROSEC(fc_duration);
				}

				PendingWalStats.wal_write++;

				if (fc_written <= 0)
				{
					char		fc_xlogfname[MAXFNAMELEN];
					int			fc_save_errno;

					if (errno == EINTR)
						continue;

					fc_save_errno = errno;
					XLogFileName(fc_xlogfname, fc_tli, openLogSegNo,
								 wal_segment_size);
					errno = fc_save_errno;
					ereport(PANIC,
							(errcode_for_file_access(),
							 errmsg("could not write to log file %s "
									"at offset %u, length %zu: %m",
									fc_xlogfname, fc_startoffset, fc_nleft)));
				}
				fc_nleft -= fc_written;
				fc_from += fc_written;
				fc_startoffset += fc_written;
			} while (fc_nleft > 0);

			fc_npages = 0;

			/*
			 * 如果我们刚刚写入了日志文件段的整个最后一页，
			 * 立即对该段进行 fsync。这避免了在后续 fsync 请求到来时
			 * 返回并重新打开先前段的问题。在这里进行此操作确保
			 * 只有一个后端会执行此 fsync。
			 *
			 * 这也是通知存档器该段准备好复制到归档存储的合适位置，
			 * 并更新 archive_timeout 的计时器，如果自上次
			 * 检查点以来使用了太多日志文件段，则发出检查点信号。
			 */
			if (fc_finishing_seg)
			{
				issue_xlog_fsync(openLogFile, openLogSegNo, fc_tli);

				/* 信号，表示我们需要稍后唤醒 walsenders */
				WalSndWakeupRequest();

				LogwrtResult.Flush = LogwrtResult.Write;	/* 页的结束 */

				if (XLogArchivingActive())
					XLogArchiveNotifySeg(openLogSegNo, fc_tli);

				XLogCtl->lastSegSwitchTime = (pg_time_t) time(NULL);
				XLogCtl->lastSegSwitchLSN = LogwrtResult.Flush;

				/*
				 * 如果自上次检查点以来我们消耗了太多的 xlog，
				 * 则请求一个检查点。为了加快速度，我们首先使用
				 * RedoRecPtr 的本地副本进行检查，该副本可能已过时；
				 * 如果看起来需要检查点，强制更新 RedoRecPtr 并
				 * 重新检查。
				 */
				if (IsUnderPostmaster && XLogCheckpointNeeded(openLogSegNo))
				{
					(void) GetRedoRecPtr();
					if (XLogCheckpointNeeded(openLogSegNo))
						RequestCheckpoint(CHECKPOINT_CAUSE_XLOG);
				}
			}
		}

		if (fc_ispartialpage)
		{
			/* 只要求写入部分页面 */
			LogwrtResult.Write = fc_WriteRqst.Write;
			break;
		}
		fc_curridx = NextBufIdx(fc_curridx);

		/* 如果灵活，写入后立即退出循环 */
		if (fc_flexible && fc_npages == 0)
			break;
	}

	Assert(fc_npages == 0);

	/*
	 * 如果要求刷新，就执行
	 */
	if (LogwrtResult.Flush < fc_WriteRqst.Flush &&
		LogwrtResult.Flush < LogwrtResult.Write)
	{
		/*
		 * 可能在没有迭代上述循环的情况下到达这里，在这种情况下我们可能
		 * 没有打开文件或打开了错误的文件。然而，我们不需要
		 * fsync 多于一个文件。
		 */
		if (sync_method != SYNC_METHOD_OPEN &&
			sync_method != SYNC_METHOD_OPEN_DSYNC)
		{
			if (openLogFile >= 0 &&
				!XLByteInPrevSeg(LogwrtResult.Write, openLogSegNo,
								 wal_segment_size))
				fc_XLogFileClose();
			if (openLogFile < 0)
			{
				XLByteToPrevSeg(LogwrtResult.Write, openLogSegNo,
								wal_segment_size);
				openLogTLI = fc_tli;
				openLogFile = XLogFileOpen(openLogSegNo, fc_tli);
				ReserveExternalFD();
			}

			issue_xlog_fsync(openLogFile, openLogSegNo, fc_tli);
		}

		/* 信号，表示我们需要稍后唤醒 walsenders */
		WalSndWakeupRequest();

		LogwrtResult.Flush = LogwrtResult.Write;
	}

	/*
	 * 更新共享内存状态
	 *
	 * 我们确保共享的 'request' 值不落后于
	 * 'result' 值。这不是绝对必要的，但可以节省
	 * 在几个地方的代码。
	 */
	{
		SpinLockAcquire(&XLogCtl->info_lck);
		XLogCtl->LogwrtResult = LogwrtResult;
		if (XLogCtl->LogwrtRqst.Write < LogwrtResult.Write)
			XLogCtl->LogwrtRqst.Write = LogwrtResult.Write;
		if (XLogCtl->LogwrtRqst.Flush < LogwrtResult.Flush)
			XLogCtl->LogwrtRqst.Flush = LogwrtResult.Flush;
		SpinLockRelease(&XLogCtl->info_lck);
	}
}

/*
 * 记录用于异步事务提交/回滚的 LSN
 * 并在需要时提示 WALWriter 进行工作。
 * （这不应用于同步提交。）
 */
void XLogSetAsyncXactLSN(XLogRecPtr fc_asyncXactLSN)
{
	XLogRecPtr	fc_WriteRqstPtr = fc_asyncXactLSN;
	bool		fc_sleeping;

	SpinLockAcquire(&XLogCtl->info_lck);
	LogwrtResult = XLogCtl->LogwrtResult;
	fc_sleeping = XLogCtl->WalWriterSleeping;
	if (XLogCtl->asyncXactLSN < fc_asyncXactLSN)
		XLogCtl->asyncXactLSN = fc_asyncXactLSN;
	SpinLockRelease(&XLogCtl->info_lck);

	/*
	 * 如果 WALWriter 正在睡眠，我们应该将其唤醒，以使其能够退出
	 * 低功耗模式。否则，确定是否有可用于写入的完整 WAL 页面。
	 */
	if (!fc_sleeping)
	{
		/* 回滚到上一个完成的页面边界 */
		fc_WriteRqstPtr -= fc_WriteRqstPtr % XLOG_BLCKSZ;

		/* 如果我们已经刷新到这一点，就完成了 */
		if (fc_WriteRqstPtr <= LogwrtResult.Flush)
			return;
	}

	/*
	 * 提示 WALWriter：它有一整页的 WAL 要写入，或者我们希望它
	 * 退出低功耗模式，以便此异步提交能够在预期的时间内写入磁盘。
	 */
	if (ProcGlobal->walwriterLatch)
		SetLatch(ProcGlobal->walwriterLatch);
}

/*
 * 记录可以删除的 WAL 的 LSN，因为它不被
 * 任何复制槽所需。
 */
void XLogSetReplicationSlotMinimumLSN(XLogRecPtr fc_lsn)
{
	SpinLockAcquire(&XLogCtl->info_lck);
	XLogCtl->replicationSlotMinLSN = fc_lsn;
	SpinLockRelease(&XLogCtl->info_lck);
}


/*
 * 返回我们必须保留的最旧 LSN，以满足某些
 * 复制槽的需求。
 */
static XLogRecPtr fc_XLogGetReplicationSlotMinimumLSN(void)
{
	XLogRecPtr	fc_retval;

	SpinLockAcquire(&XLogCtl->info_lck);
	fc_retval = XLogCtl->replicationSlotMinLSN;
	SpinLockRelease(&XLogCtl->info_lck);

	return fc_retval;
}

/*
 * 在控制文件中推进 minRecoveryPoint。
 *
 * 如果我们在恢复期间崩溃，必须在数据库一致之前再次达到这一点。
 *
 * 如果 'force' 为 true，则忽略 'lsn' 参数。否则，仅当 minRecoveryPoint 
 * 尚未大于或等于 'lsn' 时，才会更新 minRecoveryPoint。
 */
static void fc_UpdateMinRecoveryPoint(XLogRecPtr fc_lsn, bool fc_force)
{
	/* 使用我们本地副本进行快速检查 */
	if (!updateMinRecoveryPoint || (!fc_force && fc_lsn <= LocalMinRecoveryPoint))
		return;

	/*
	 * 无效的 minRecoveryPoint 意味着我们需要恢复所有的 WAL，
	 * 即，我们正在进行崩溃恢复。在那种情况下，我们永远不会修改控制文件的
	 * 值，因此我们也可以短路将来的检查。minRecoveryPoint 和 
	 * minRecoveryPointTLI 的本地值不应在崩溃恢复完成之前更新。
	 * 我们仅在启动过程中这样做，因为它在完成崩溃恢复之前不应更新
	 * 自身的 minRecoveryPoint 参考，以确保在这种情况下所有可用的 WAL
	 * 都将被重放。这也能避免启动过程对控制文件进行额外的锁定。
	 */
	if (XLogRecPtrIsInvalid(LocalMinRecoveryPoint) && InRecovery)
	{
		updateMinRecoveryPoint = false;
		return;
	}

	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);

	/* 更新本地副本 */
	LocalMinRecoveryPoint = ControlFile->minRecoveryPoint;
	LocalMinRecoveryPointTLI = ControlFile->minRecoveryPointTLI;

	if (XLogRecPtrIsInvalid(LocalMinRecoveryPoint))
		updateMinRecoveryPoint = false;
	else if (fc_force || LocalMinRecoveryPoint < fc_lsn)
	{
		XLogRecPtr	fc_newMinRecoveryPoint;
		TimeLineID	fc_newMinRecoveryPointTLI;

		/*
		 * 为了避免过于频繁地更新控制文件，我们将其更新
		 * 到最后一个被重播的记录，尽管 'lsn' 对于正确性来说已足够。
		 * 这也使得 'force' 情况不需要有效的 'lsn' 值。
		 *
		 * 这样做的另一个重要原因是传入的 'lsn' 值可能是虚假的，即
		 * 超过了可用 WAL 的末尾，如果调用者是从一个损坏的堆页面
		 * 获取的。接受这样的值作为最小恢复点将阻止我们完全启动。
		 * 相反，我们只是记录一个警告并继续恢复。
		 * （另见关于损坏 LSN 的注释在 XLogFlush 中。）
		 */
		fc_newMinRecoveryPoint = GetCurrentReplayRecPtr(&fc_newMinRecoveryPointTLI);
		if (!fc_force && fc_newMinRecoveryPoint < fc_lsn)
			elog(WARNING,
				 "xlog min recovery request %X/%X is past current point %X/%X",
				 LSN_FORMAT_ARGS(fc_lsn), LSN_FORMAT_ARGS(fc_newMinRecoveryPoint));

		/* 更新控制文件 */
		if (ControlFile->minRecoveryPoint < fc_newMinRecoveryPoint)
		{
			ControlFile->minRecoveryPoint = fc_newMinRecoveryPoint;
			ControlFile->minRecoveryPointTLI = fc_newMinRecoveryPointTLI;
			fc_UpdateControlFile();
			LocalMinRecoveryPoint = fc_newMinRecoveryPoint;
			LocalMinRecoveryPointTLI = fc_newMinRecoveryPointTLI;

			ereport(DEBUG2,
					(errmsg_internal("updated min recovery point to %X/%X on timeline %u",
									 LSN_FORMAT_ARGS(fc_newMinRecoveryPoint),
									 fc_newMinRecoveryPointTLI)));
		}
	}
	LWLockRelease(ControlFileLock);
}

/*
 * 确保在给定位置的所有 XLOG 数据都刷新到磁盘。
 *
 * 注意：这与 XLogWrite 主要的不同在于 WALWriteLock 尚未被持有，
 * 而我们尽量避免获取它。
 */
void XLogFlush(XLogRecPtr fc_record)
{
	XLogRecPtr	fc_WriteRqstPtr;
	XLogwrtRqst fc_WriteRqst;
	TimeLineID	fc_insertTLI = XLogCtl->InsertTimeLineID;

	/*
	 * 在 REDO 期间，我们是在读取而不是写入 WAL。因此，我们应该
	 * 更新 minRecoveryPoint 而不是尝试刷新 WAL。我们测试
	 * XLogInsertAllowed()，而不是 InRecovery，因为我们也需要
	 * 检查点进程以这种方式行事，而且当它尝试写入
	 * 恢复结束检查点时，它确实应该刷新。
	 */
	if (!XLogInsertAllowed())
	{
		fc_UpdateMinRecoveryPoint(fc_record, false);
		return;
	}

	/* 如果已经知道已刷新，则快速退出 */
	if (fc_record <= LogwrtResult.Flush)
		return;

#ifdef WAL_DEBUG
	if (XLOG_DEBUG)
		elog(LOG, "xlog flush request %X/%X; write %X/%X; flush %X/%X",
			 LSN_FORMAT_ARGS(fc_record),
			 LSN_FORMAT_ARGS(LogwrtResult.Write),
			 LSN_FORMAT_ARGS(LogwrtResult.Flush));
#endif

	START_CRIT_SECTION();

	/*
	 * 由于 fsync 通常是一个非常耗费资源的操作，我们尽量将尽可能多的数据
	 * 依附在每个 fsync 上：如果我们看到 xlog 缓冲区中有更多数据进入，
	 * 我们会写入并刷新它，以便 LogwrtResult.Flush 的最终值
	 * 尽可能大。这给了我们避免在后续立即 fsync 的机会。
	 */

	/* 初始化到给定目标；下面可能会增加 */
	fc_WriteRqstPtr = fc_record;

	/*
	 * 现在等待直到我们获得写入锁，或者其他人为我们执行刷新操作。
	 */
	for (;;)
	{
		XLogRecPtr	fc_insertpos;

		/* 读取 LogwrtResult 并更新本地状态 */
		SpinLockAcquire(&XLogCtl->info_lck);
		if (fc_WriteRqstPtr < XLogCtl->LogwrtRqst.Write)
			fc_WriteRqstPtr = XLogCtl->LogwrtRqst.Write;
		LogwrtResult = XLogCtl->LogwrtResult;
		SpinLockRelease(&XLogCtl->info_lck);

		/* 已完成？ */
		if (fc_record <= LogwrtResult.Flush)
			break;

		/*
		 * 在实际执行写入之前，等待所有正在进行的插入
		 * 完成我们即将写入的页面。
		 */
		fc_insertpos = fc_WaitXLogInsertionsToFinish(fc_WriteRqstPtr);

		/*
		 * 尝试获取写入锁。如果我们无法立即获取，等待
		 * 直到它被释放，然后重新检查我们是否仍然需要执行刷新
		 * 或者持锁的后端是否已经为我们执行了刷新。这
		 * 有助于在系统瓶颈由 fsync 的速度时保持良好的组提交速率。
		 */
		if (!LWLockAcquireOrWait(WALWriteLock, LW_EXCLUSIVE))
		{
			/*
			 * 锁现在是免费的，但我们还没有获取它。在我们
			 * 获取之前，循环检查是否已经有其他人为我们刷新
			 * 了记录。
			 */
			continue;
		}

		/* 获得锁；重新检查请求是否满足 */
		LogwrtResult = XLogCtl->LogwrtResult;
		if (fc_record <= LogwrtResult.Flush)
		{
			LWLockRelease(WALWriteLock);
			break;
		}

		/*
		 * 刷新前睡眠！通过在这里添加延迟，我们可能会给其他
		 * 后端提供机会，加入组提交跟随者的积压中；这可以显著提高
		 * 事务吞吐量，但有可能增加事务延迟的风险。
		 *
		 * 如果没有开启enableFsync，或者活动事务的其他后端少于
		 * CommitSiblings，则我们不会睡眠。
		 */
		if (CommitDelay > 0 && enableFsync &&
			MinimumActiveBackends(CommitSiblings))
		{
			pg_usleep(CommitDelay);

			/*
			 * 重新检查我们现在可以刷新WAL的程度。一般来说，在保持
			 * WALWriteLock的情况下调用WaitXLogInsertionsToFinish并不
			 * 安全，因为进行中的插入可能还需要获取WALWriteLock以
			 * 进展。但我们知道直到insertpos的所有插入都已经完成了，
			 * 因为这正是之前的WaitXLogInsertionsToFinish()返回的。
			 * 我们再次调用它只是为了允许insertpos向前移动，而不是
			 * 真的等任何人。
			 */
			fc_insertpos = fc_WaitXLogInsertionsToFinish(fc_insertpos);
		}

		/* 尝试将后续的添加写入/刷新到XLOG中 */
		fc_WriteRqst.Write = fc_insertpos;
		fc_WriteRqst.Flush = fc_insertpos;

		fc_XLogWrite(fc_WriteRqst, fc_insertTLI, false);

		LWLockRelease(WALWriteLock);
		/* 完成 */
		break;
	}

	END_CRIT_SECTION();

	/* 现在唤醒walsenders，因为我们已释放了高度竞争的锁 */
	WalSndWakeupProcessRequests();

	/*
	 * 如果我们仍然没有刷新到请求点，那么我们就遇到问题了；
	 * 最有可能的情况是，请求的刷新点超出了XLOG的末尾。
	 * 这种情况发生在磁盘页面有损坏的LSN时。
	 *
	 * 过去我们将其视为PANIC条件，但这会损害系统的鲁棒性，
	 * 而不是帮助它：我们不希望由于一个数据页面的损坏而导致
	 * 整个系统崩溃。特别是，如果在恢复过程中再次遇到损坏的
	 * 页面，那我们将根本无法重启数据库！(这种情况在实际
	 * 环境中确实在7.1版本中发生过几次。)自8.4起，在恢复过程中
	 * 遇到的坏LSN是UpdateMinRecoveryPoint的问题；我们在恢复
	 * 期间到达这里的唯一时机是在刷新结束恢复检查点记录时，
	 * 我们不期望那会有坏LSN。
	 *
	 * 注意，对于来自xact.c的调用，ERROR会被提升为PANIC，
	 * 因为xact.c在关键区域内调用此例程。然而，来自bufmgr.c的
	 * 调用不在关键区域内，因此我们不会强制因数据页面上的
	 * 坏LSN而重启。
	 */
	if (LogwrtResult.Flush < fc_record)
		elog(ERROR,
			 "xlog flush request %X/%X is not satisfied --- flushed only to %X/%X",
			 LSN_FORMAT_ARGS(fc_record),
			 LSN_FORMAT_ARGS(LogwrtResult.Flush));
}

/*
 * 写入并刷新xlog，但不具体指定写入位置。
 *
 * 我们通常只写入完成的块；但如果在此基础上没有任何操作，我们会检查当前未完成块中的未写入异步提交，并写入最新的一个。因此，如果未使用异步提交，我们只会写入完整的块。
 *
 * 如果根据上述情况有任何需要写入的内容，我们会立即执行。但是，为了避免以会影响并发IO的速率调用fsync、fdatasync等，我们仅每wal_writer_delay毫秒刷新一次WAL，或在未刷新的块超过wal_writer_flush_after时刷新。
 *
 * 我们可以保证异步提交在最多三个wal_writer_delay周期后到达磁盘。（在刷新完整块时，我们允许XLogWrite“灵活”写入，这意味着它可以在缓冲区环的末尾停止；这在非常高负载或长wal_writer_delay的情况下会产生影响，但在异步提交的最坏情况下会附加一个额外的周期。）
 *
 * 此例程由后台walwriter进程定期调用。
 *
 * 如果有任何工作需要完成，即使因为wal_writer_delay/wal_writer_flush_after跳过了刷新，也返回true。
 */
bool XLogBackgroundFlush(void)
{
	XLogwrtRqst fc_WriteRqst;
	bool		fc_flexible = true;
	static TimestampTz fc_lastflush;
	TimestampTz fc_now;
	int			fc_flushbytes;
	TimeLineID	fc_insertTLI;

	/* XLOG在恢复期间不需要刷新 */
	if (RecoveryInProgress())
		return false;

	/*
	 * 由于我们不在恢复中，InsertTimeLineID已设置且不能更改，
	 * 因此我们可以在没有锁的情况下读取它。
	 */
	fc_insertTLI = XLogCtl->InsertTimeLineID;

	/* 读取 LogwrtResult 并更新本地状态 */
	SpinLockAcquire(&XLogCtl->info_lck);
	LogwrtResult = XLogCtl->LogwrtResult;
	fc_WriteRqst = XLogCtl->LogwrtRqst;
	SpinLockRelease(&XLogCtl->info_lck);

	/* 回滚到上一个完成的页面边界 */
	fc_WriteRqst.Write -= fc_WriteRqst.Write % XLOG_BLCKSZ;

	/* 如果我们已经刷新到那儿，则考虑异步提交记录 */
	if (fc_WriteRqst.Write <= LogwrtResult.Flush)
	{
		SpinLockAcquire(&XLogCtl->info_lck);
		fc_WriteRqst.Write = XLogCtl->asyncXactLSN;
		SpinLockRelease(&XLogCtl->info_lck);
		fc_flexible = false;		/* 确保所有内容都被写入 */
	}

	/*
	 * 如果已知已刷新，那么我们完成了。只需检查我们是否
	 * 持有一个对不再使用的日志文件的打开文件句柄，
	 * 以防止该文件被删除。
	 */
	if (fc_WriteRqst.Write <= LogwrtResult.Flush)
	{
		if (openLogFile >= 0)
		{
			if (!XLByteInPrevSeg(LogwrtResult.Write, openLogSegNo,
								 wal_segment_size))
			{
				fc_XLogFileClose();
			}
		}
		return false;
	}

	/*
	 * 根据wal_writer_delay和wal_writer_flush_after GUCs
	 * 确定要刷新WAL的程度。
	 */
	fc_now = GetCurrentTimestamp();
	fc_flushbytes =
		fc_WriteRqst.Write / XLOG_BLCKSZ - LogwrtResult.Flush / XLOG_BLCKSZ;

	if (WalWriterFlushAfter == 0 || fc_lastflush == 0)
	{
		/* 第一次调用，或基于块的限制已禁用 */
		fc_WriteRqst.Flush = fc_WriteRqst.Write;
		fc_lastflush = fc_now;
	}
	else if (TimestampDifferenceExceeds(fc_lastflush, fc_now, WalWriterDelay))
	{
		/*
		 * 至少每WalWriterDelay毫秒刷新一次写入。这对于限制
		 * 异步提交到达磁盘所需的时间非常重要。
		 */
		fc_WriteRqst.Flush = fc_WriteRqst.Write;
		fc_lastflush = fc_now;
	}
	else if (fc_flushbytes >= WalWriterFlushAfter)
	{
		/* 超过wal_writer_flush_after块，刷新 */
		fc_WriteRqst.Flush = fc_WriteRqst.Write;
		fc_lastflush = fc_now;
	}
	else
	{
		/* 此次不刷新 */
		fc_WriteRqst.Flush = 0;
	}

#ifdef WAL_DEBUG
	if (XLOG_DEBUG)
		elog(LOG, "xlog bg flush request write %X/%X; flush: %X/%X, current is write %X/%X; flush %X/%X",
			 LSN_FORMAT_ARGS(fc_WriteRqst.Write),
			 LSN_FORMAT_ARGS(fc_WriteRqst.Flush),
			 LSN_FORMAT_ARGS(LogwrtResult.Write),
			 LSN_FORMAT_ARGS(LogwrtResult.Flush));
#endif

	START_CRIT_SECTION();

	/* 现在等待任何正在进行的插入完成并获取写锁 */
	fc_WaitXLogInsertionsToFinish(fc_WriteRqst.Write);
	LWLockAcquire(WALWriteLock, LW_EXCLUSIVE);
	LogwrtResult = XLogCtl->LogwrtResult;
	if (fc_WriteRqst.Write > LogwrtResult.Write ||
		fc_WriteRqst.Flush > LogwrtResult.Flush)
	{
		fc_XLogWrite(fc_WriteRqst, fc_insertTLI, fc_flexible);
	}
	LWLockRelease(WALWriteLock);

	END_CRIT_SECTION();

	/* 现在唤醒walsenders，因为我们已释放了高度竞争的锁 */
	WalSndWakeupProcessRequests();

	/*
	 * 很好，完成。为了减轻关键路径的工作量，尽量初始化
	 * 尽可能多的不再需要的WAL缓冲区以供将来使用。
	 */
	fc_AdvanceXLInsertBuffer(InvalidXLogRecPtr, fc_insertTLI, true);

	/*
	 * 如果我们确定需要写入数据，但其他人
	 * 已经写入/刷新，则应将其视为活跃状态，以
	 * 避免过早休眠。
	 */
	return true;
}

/*
 * 测试XLOG数据是否已刷新到（至少）给定位置。
 *
 * 如果仍需要刷新，则返回true。（可能会有其他人
 * 已经在刷新到那个位置的过程中。）
 */
bool XLogNeedsFlush(XLogRecPtr fc_record)
{
	/*
	 * 在恢复期间，我们不刷新 WAL，而是更新 minRecoveryPoint
	 * 。因此，“需要刷新”被认为是 minRecoveryPoint
	 * 是否需要更新。
	 */
	if (RecoveryInProgress())
	{
		/*
		 * 无效的 minRecoveryPoint 意味着我们需要恢复所有的
		 * WAL，即我们正在进行崩溃恢复。在这种情况下，我们从不修改控制
		 * 文件的值，因此我们也可以跳过未来的检查。这为启动过程触发了快速退出路径，
		 * 只要它在进行崩溃恢复时没有重放所有可用的 WAL，就不能更新其本地副本的 minRecoveryPoint。
		 */
		if (XLogRecPtrIsInvalid(LocalMinRecoveryPoint) && InRecovery)
			updateMinRecoveryPoint = false;

		/* 如果已知已更新或无法更新则快速退出 */
		if (fc_record <= LocalMinRecoveryPoint || !updateMinRecoveryPoint)
			return false;

		/*
		 * 更新 minRecoveryPoint 的本地副本。但如果锁被占用，
		 * 则返回保守的估计。
		 */
		if (!LWLockConditionalAcquire(ControlFileLock, LW_SHARED))
			return true;
		LocalMinRecoveryPoint = ControlFile->minRecoveryPoint;
		LocalMinRecoveryPointTLI = ControlFile->minRecoveryPointTLI;
		LWLockRelease(ControlFileLock);

		/*
		 * 检查 minRecoveryPoint 的其他进程，而不是正在进行崩溃恢复的启动
		 * 过程，如果崩溃恢复仍在进行，则不应更新控制
		 * 文件值。
		 */
		if (XLogRecPtrIsInvalid(LocalMinRecoveryPoint))
			updateMinRecoveryPoint = false;

		/* 再次检查 */
		if (fc_record <= LocalMinRecoveryPoint || !updateMinRecoveryPoint)
			return false;
		else
			return true;
	}

	/* 如果已经知道已刷新，则快速退出 */
	if (fc_record <= LogwrtResult.Flush)
		return false;

	/* 读取 LogwrtResult 并更新本地状态 */
	SpinLockAcquire(&XLogCtl->info_lck);
	LogwrtResult = XLogCtl->LogwrtResult;
	SpinLockRelease(&XLogCtl->info_lck);

	/* 再次检查 */
	if (fc_record <= LogwrtResult.Flush)
		return false;

	return true;
}

/*
 * 尝试使给定的 XLOG 文件段存在。
 *
 * logsegno: 标识段。
 *
 * *added: 返回时，如果此调用增加了现有段的数量则为真。
 *
 * path: 返回时，这个 char[MAXPGPATH] 包含 logsegno 文件的路径。
 *
 * 返回 -1 或打开文件的 FD。这里的 -1 不是一个错误；想要打开段的调用者
 * 应该尝试打开“path”，通常会成功。 （这有点奇怪，但对调用者来说效率高。）
 */
static int fc_XLogFileInitInternal(XLogSegNo fc_logsegno, TimeLineID fc_logtli,
					 bool *fc_added, char *fc_path)
{
	char		fc_tmppath[MAXPGPATH];
	PGAlignedXLogBlock fc_zbuffer;
	XLogSegNo	fc_installed_segno;
	XLogSegNo	fc_max_segno;
	int			fc_fd;
	int			fc_save_errno;

	Assert(fc_logtli != 0);

	XLogFilePath(fc_path, fc_logtli, fc_logsegno, wal_segment_size);

	/*
	 * 尝试使用现有文件（检查点生成器可能已经创建了它）
	 */
	*fc_added = false;
	fc_fd = BasicOpenFile(fc_path, O_RDWR | PG_BINARY | fc_get_sync_bit(sync_method));
	if (fc_fd < 0)
	{
		if (errno != ENOENT)
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not open file \"%s\": %m", fc_path)));
	}
	else
		return fc_fd;

	/*
	 * 初始化一个空的（全零）段。注意：可能有其他进程也在做同样的事情。
	 * 如果是这样的话，我们将最终预创建一个额外的日志段。这似乎也可以，且
	 * 比在整个漫长的过程中保持持锁更好。
	 */
	elog(DEBUG2, "creating and filling new WAL file");

	snprintf(fc_tmppath, MAXPGPATH, XLOGDIR "/xlogtemp.%d", (int) getpid());

	unlink(fc_tmppath);

	/* 不要在这里使用 get_sync_bit() --- 只想在填充结束时进行 fsync */
	fc_fd = BasicOpenFile(fc_tmppath, O_RDWR | O_CREAT | O_EXCL | PG_BINARY);
	if (fc_fd < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not create file \"%s\": %m", fc_tmppath)));

	memset(fc_zbuffer.data, 0, XLOG_BLCKSZ);

	pgstat_report_wait_start(WAIT_EVENT_WAL_INIT_WRITE);
	fc_save_errno = 0;
	if (wal_init_zero)
	{
		struct iovec fc_iov[PG_IOV_MAX];
		int			fc_blocks;

		/*
		 * 将文件填充为零。使用此设置，我们以困难的方式执行此操作，以
		 * 确保所有文件空间确实已被分配。在允许文件中“孔”的平台上，仅仅寻址到最后
		 * 并不会分配中间空间。这样，我们知道我们有足够的空间，并且（在下面的 fsync 之后）
		 * 所有间接块都写入磁盘。因此，fdatasync(2) 或 O_DSYNC 将足以同步对日志文件的未来写入。
		 */

		/* 准备一次写出我们零缓冲区的许多副本。 */
		for (int fc_i = 0; fc_i < lengthof(fc_iov); ++fc_i)
		{
			fc_iov[fc_i].iov_base = fc_zbuffer.data;
			fc_iov[fc_i].iov_len = XLOG_BLCKSZ;
		}

		/* 循环中，每次系统调用写入尽可能多的块。 */
		fc_blocks = wal_segment_size / XLOG_BLCKSZ;
		for (int fc_i = 0; fc_i < fc_blocks;)
		{
			int			fc_iovcnt = Min(fc_blocks - fc_i, lengthof(fc_iov));
			off_t		fc_offset = fc_i * XLOG_BLCKSZ;

			if (pg_pwritev_with_retry(fc_fd, fc_iov, fc_iovcnt, fc_offset) < 0)
			{
				fc_save_errno = errno;
				break;
			}

			fc_i += fc_iovcnt;
		}
	}
	else
	{
		/*
		 * 否则，寻址到最后并写入一个孤立的字节就足够了。
		 */
		errno = 0;
		if (pg_pwrite(fc_fd, fc_zbuffer.data, 1, wal_segment_size - 1) != 1)
		{
			/* 如果写入没有设置 errno，假设没有磁盘空间 */
			fc_save_errno = errno ? errno : ENOSPC;
		}
	}
	pgstat_report_wait_end();

	if (fc_save_errno)
	{
		/*
		 * 如果我们未能创建文件，请删除它以释放磁盘空间
		 */
		unlink(fc_tmppath);

		close(fc_fd);

		errno = fc_save_errno;

		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not write to file \"%s\": %m", fc_tmppath)));
	}

	pgstat_report_wait_start(WAIT_EVENT_WAL_INIT_SYNC);
	if (pg_fsync(fc_fd) != 0)
	{
		int			fc_save_errno = errno;

		close(fc_fd);
		errno = fc_save_errno;
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not fsync file \"%s\": %m", fc_tmppath)));
	}
	pgstat_report_wait_end();

	if (close(fc_fd) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_tmppath)));

	/*
	 * 现在将段移到它的最终名称位置。处理
	 * 在我们填充我们的文件时，可能有人已经创建了文件的可能性：如果是这样，用
	 * 我们的文件来预创建一个未来的日志段。
	 */
	fc_installed_segno = fc_logsegno;

	/*
	 * XXX：我们应该使用什么作为 max_segno？我们曾经使用 XLOGfileslop，当
	 * 那是一个常量时，但这总是有点可疑：通常，在一个检查点上，XLOGfileslop 是
	 * 从检查点记录的偏移量，但在这里，它是从插入位置的偏移量。我们无法在这里
	 * 做正常的 XLOGfileslop 计算，因为我们无法访问
	 * 先前检查点的重做位置。因此稍微任意地，只使用
	 * CheckPointSegments。
	 */
	fc_max_segno = fc_logsegno + CheckPointSegments;
	if (fc_InstallXLogFileSegment(&fc_installed_segno, fc_tmppath, true, fc_max_segno,
							   fc_logtli))
	{
		*fc_added = true;
		elog(DEBUG2, "done creating and filling new WAL file");
	}
	else
	{
		/*
		 * 不需要更多未来的段，或者 InstallXLogFileSegment()
		 * 未能将文件重命名到正确的位置。如果重命名失败，打开文件的
		 * 调用者可能会失败。
		 */
		unlink(fc_tmppath);
		elog(DEBUG2, "abandoned new WAL file");
	}

	return -1;
}

/*
 * 创建一个新的 XLOG 文件段，或打开一个现有的文件段。
 *
 * logsegno：识别要创建/打开的段。
 *
 * 返回打开文件的 FD。
 *
 * 注意：这里的错误是 ERROR 而不是 PANIC，因为我们可能在
 * 关键部分内（例如，在检查点期间，失败时没有理由关闭系统）。
 * 如果我们在关键部分内，它们将提升为 PANIC。
 */
int XLogFileInit(XLogSegNo fc_logsegno, TimeLineID fc_logtli)
{
	bool		fc_ignore_added;
	char		fc_path[MAXPGPATH];
	int			fc_fd;

	Assert(fc_logtli != 0);

	fc_fd = fc_XLogFileInitInternal(fc_logsegno, fc_logtli, &fc_ignore_added, fc_path);
	if (fc_fd >= 0)
		return fc_fd;

	/* 现在打开原始目标段（可能不是我刚刚创建的文件） */
	fc_fd = BasicOpenFile(fc_path, O_RDWR | PG_BINARY | fc_get_sync_bit(sync_method));
	if (fc_fd < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\": %m", fc_path)));
	return fc_fd;
}

/*
 * 通过复制一个现有的 XLOG 文件段来创建一个新的文件段。
 *
 * destsegno：识别要创建的段。
 *
 * srcTLI，srcsegno：识别要复制的段（可以来自
 *		不同的时间线）
 *
 * upto：要复制的源文件的多少（其余部分用
 *		零填充）
 *
 * 目前这仅在恢复期间使用，因此没有锁定
 * 考虑。但是我们应该像 XLogFileInit 一样紧张，以避免
 * 放置一个无效的文件。
 */
static void fc_XLogFileCopy(TimeLineID fc_destTLI, XLogSegNo fc_destsegno,
			 TimeLineID fc_srcTLI, XLogSegNo fc_srcsegno,
			 int fc_upto)
{
	char		fc_path[MAXPGPATH];
	char		fc_tmppath[MAXPGPATH];
	PGAlignedXLogBlock fc_buffer;
	int			fc_srcfd;
	int			fc_fd;
	int			fc_nbytes;

	/*
	 * 打开源文件
	 */
	XLogFilePath(fc_path, fc_srcTLI, fc_srcsegno, wal_segment_size);
	fc_srcfd = OpenTransientFile(fc_path, O_RDONLY | PG_BINARY);
	if (fc_srcfd < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\": %m", fc_path)));

	/*
	 * 复制到临时文件名中。
	 */
	snprintf(fc_tmppath, MAXPGPATH, XLOGDIR "/xlogtemp.%d", (int) getpid());

	unlink(fc_tmppath);

	/* 不要在这里使用 get_sync_bit() --- 只想在填充结束时进行 fsync */
	fc_fd = OpenTransientFile(fc_tmppath, O_RDWR | O_CREAT | O_EXCL | PG_BINARY);
	if (fc_fd < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not create file \"%s\": %m", fc_tmppath)));

	/*
	 * 进行数据复制。
	 */
	for (fc_nbytes = 0; fc_nbytes < wal_segment_size; fc_nbytes += sizeof(fc_buffer))
	{
		int			fc_nread;

		fc_nread = fc_upto - fc_nbytes;

		/*
		 * 从源文件未读取的部分用
		 * 零填充。
		 */
		if (fc_nread < sizeof(fc_buffer))
			memset(fc_buffer.data, 0, sizeof(fc_buffer));

		if (fc_nread > 0)
		{
			int			fc_r;

			if (fc_nread > sizeof(fc_buffer))
				fc_nread = sizeof(fc_buffer);
			pgstat_report_wait_start(WAIT_EVENT_WAL_COPY_READ);
			fc_r = read(fc_srcfd, fc_buffer.data, fc_nread);
			if (fc_r != fc_nread)
			{
				if (fc_r < 0)
					ereport(ERROR,
							(errcode_for_file_access(),
							 errmsg("could not read file \"%s\": %m",
									fc_path)));
				else
					ereport(ERROR,
							(errcode(ERRCODE_DATA_CORRUPTED),
							 errmsg("could not read file \"%s\": read %d of %zu",
									fc_path, fc_r, (Size) fc_nread)));
			}
			pgstat_report_wait_end();
		}
		errno = 0;
		pgstat_report_wait_start(WAIT_EVENT_WAL_COPY_WRITE);
		if ((int) write(fc_fd, fc_buffer.data, sizeof(fc_buffer)) != (int) sizeof(fc_buffer))
		{
			int			fc_save_errno = errno;

			/*
			 * 如果我们无法创建文件，请删除它以释放磁盘空间
			 */
			unlink(fc_tmppath);
			/* 如果写入未设置 errno，假设问题是没有磁盘空间 */
			errno = fc_save_errno ? fc_save_errno : ENOSPC;

			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not write to file \"%s\": %m", fc_tmppath)));
		}
		pgstat_report_wait_end();
	}

	pgstat_report_wait_start(WAIT_EVENT_WAL_COPY_SYNC);
	if (pg_fsync(fc_fd) != 0)
		ereport(data_sync_elevel(ERROR),
				(errcode_for_file_access(),
				 errmsg("could not fsync file \"%s\": %m", fc_tmppath)));
	pgstat_report_wait_end();

	if (CloseTransientFile(fc_fd) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_tmppath)));

	if (CloseTransientFile(fc_srcfd) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_path)));

	/*
	 * 现在将段移到它的最终名称位置。
	 */
	if (!fc_InstallXLogFileSegment(&fc_destsegno, fc_tmppath, false, 0, fc_destTLI))
		elog(ERROR, "InstallXLogFileSegment should not have failed");
}

/*
 * 安装一个新的 XLOG 段文件作为当前或未来的日志段。
 *
 * 这既用于安装一个新创建的段（在创建期间具有临时文件名），也用于回收旧段。
 *
 * *segno: 确定要安装的段（或第一个可能的目标）。
 * 当 find_free 为真时，这在返回时会被修改以指示
 * 实际安装位置或最后搜索的段。
 *
 * tmppath: 要安装的文件的初始名称。它将被重命名为目标名称。
 *
 * find_free: 如果为真，则在传入的数字之后的第一个空 segno
 * 处安装新段。如果为假，则将新段安装到指定的位置，
 * 删除该位置上任何现有的段文件。
 *
 * max_segno: 安装新文件时的最大段号。如果在 *segno 和 max_segno 
 * 之间未找到空位，则失败。（当 find_free 为假时被忽略。）
 *
 * tli: 应该安装新段的时间线。
 *
 * 如果文件成功安装则返回真。假表示超出了 max_segno 限制，
 * 启动过程已暂时禁用此功能，或者在重命名文件时发生了错误。
 */
static bool fc_InstallXLogFileSegment(XLogSegNo *fc_segno, char *fc_tmppath,
					   bool fc_find_free, XLogSegNo fc_max_segno, TimeLineID fc_tli)
{
	char		fc_path[MAXPGPATH];
	struct stat fc_stat_buf;

	Assert(fc_tli != 0);

	XLogFilePath(fc_path, fc_tli, *fc_segno, wal_segment_size);

	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	if (!XLogCtl->InstallXLogFileSegmentActive)
	{
		LWLockRelease(ControlFileLock);
		return false;
	}

	if (!fc_find_free)
	{
		/* 强制安装：清除任何现有的段文件 */
		durable_unlink(fc_path, DEBUG1);
	}
	else
	{
		/* 找到一个空位以放置它 */
		while (stat(fc_path, &fc_stat_buf) == 0)
		{
			if ((*fc_segno) >= fc_max_segno)
			{
				/* 在指定范围内找不到空位 */
				LWLockRelease(ControlFileLock);
				return false;
			}
			(*fc_segno)++;
			XLogFilePath(fc_path, fc_tli, *fc_segno, wal_segment_size);
		}
	}

	Assert(access(fc_path, F_OK) != 0 && errno == ENOENT);
	if (durable_rename(fc_tmppath, fc_path, LOG) != 0)
	{
		LWLockRelease(ControlFileLock);
		/* durable_rename 已发出日志消息 */
		return false;
	}

	LWLockRelease(ControlFileLock);

	return true;
}

/*
 * 打开一个预先存在的日志文件段以进行写入。
 */
int XLogFileOpen(XLogSegNo fc_segno, TimeLineID fc_tli)
{
	char		fc_path[MAXPGPATH];
	int			fc_fd;

	XLogFilePath(fc_path, fc_tli, fc_segno, wal_segment_size);

	fc_fd = BasicOpenFile(fc_path, O_RDWR | PG_BINARY | fc_get_sync_bit(sync_method));
	if (fc_fd < 0)
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\": %m", fc_path)));

	return fc_fd;
}

/*
 * 关闭当前的日志文件段以进行写入。
 */
static void fc_XLogFileClose(void)
{
	Assert(openLogFile >= 0);

	/*
	 * 在正常操作中，WAL 段文件不会被重新读取，因此我们建议
	 * 操作系统释放任何缓存页面。但是如果正在进行 WAL 归档
	 * 或流式传输，则不要这样做，因为归档器和 walsender 进程可能
	 * 使用缓存读取 WAL 段。
	 */
#if defined(USE_POSIX_FADVISE) && defined(POSIX_FADV_DONTNEED)
	if (!XLogIsNeeded())
		(void) posix_fadvise(openLogFile, 0, 0, POSIX_FADV_DONTNEED);
#endif

	if (close(openLogFile) != 0)
	{
		char		fc_xlogfname[MAXFNAMELEN];
		int			fc_save_errno = errno;

		XLogFileName(fc_xlogfname, openLogTLI, openLogSegNo, wal_segment_size);
		errno = fc_save_errno;
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_xlogfname)));
	}

	openLogFile = -1;
	ReleaseExternalFD();
}

/*
 * 预分配超出指定日志端点的日志文件。
 *
 * XXX 目前这非常保守，因为它只强制存在一个未来的日志段，
 * 而且只有在我们完成当前段的 75% 后才会这样。这仅适用于非常低
 * WAL 量的系统。高量系统在建立足够的回收日志段之后会正常工作，
 * 但是启动瞬态可能会包括许多由前台进程创建的段，这并不好。
 *
 * XLogFileInitInternal() 可以报告错误。所有已知原因表明严重问题；
 * 例如，文件系统已满就是一个原因。检查点 WAL
 * 和/或 ControlFile 更新已完成。如果 RequestCheckpoint()
 * 发起了当前检查点，而错误结束了此函数，则调用 RequestCheckpoint()
 * 的命令失败。这并不理想，但不值得扭曲更多函数以使用调用者
 * 指定的 elevel 值。（有或没有 RequestCheckpoint()，错误会阻止一些
 * 不必要的报告和资源回收。）
 */
static void fc_PreallocXlogFiles(XLogRecPtr fc_endptr, TimeLineID fc_tli)
{
	XLogSegNo	fc__logSegNo;
	int			fc_lf;
	bool		fc_added;
	char		fc_path[MAXPGPATH];
	uint64		fc_offset;

	if (!XLogCtl->InstallXLogFileSegmentActive)
		return;					/* 解锁检查表示没有 */

	XLByteToPrevSeg(fc_endptr, fc__logSegNo, wal_segment_size);
	fc_offset = XLogSegmentOffset(fc_endptr - 1, wal_segment_size);
	if (fc_offset >= (uint32) (0.75 * wal_segment_size))
	{
		fc__logSegNo++;
		fc_lf = fc_XLogFileInitInternal(fc__logSegNo, fc_tli, &fc_added, fc_path);
		if (fc_lf >= 0)
			close(fc_lf);
		if (fc_added)
			CheckpointStats.ckpt_segs_added++;
	}
}

/*
 * 如果给定的日志段已经被移除或回收，则会抛出错误。调用者应该只传递一个它知道在服务器运行期间存在的段，因为如果自启动以来没有WAL段被移除，则此函数始终成功。'tli'仅用于错误消息中。
 *
 * 注意：此函数保证在返回时保持errno不变。这支持调用者使用此功能来可能提供有关缺失文件的更好错误消息，同时仍然能够在此返回后抛出正常的文件访问错误。
 */
void CheckXLogRemoved(XLogSegNo fc_segno, TimeLineID fc_tli)
{
	int			fc_save_errno = errno;
	XLogSegNo	fc_lastRemovedSegNo;

	SpinLockAcquire(&XLogCtl->info_lck);
	fc_lastRemovedSegNo = XLogCtl->lastRemovedSegNo;
	SpinLockRelease(&XLogCtl->info_lck);

	if (fc_segno <= fc_lastRemovedSegNo)
	{
		char		fc_filename[MAXFNAMELEN];

		XLogFileName(fc_filename, fc_tli, fc_segno, wal_segment_size);
		errno = fc_save_errno;
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("requested WAL segment %s has already been removed",
						fc_filename)));
	}
	errno = fc_save_errno;
}

/*
 * 返回最后被移除的WAL段，如果自启动以来没有段被移除，则返回0。
 *
 * 注意：结果可能会非常快过期，调用者必须处理这一点。
 */
XLogSegNo
XLogGetLastRemovedSegno(void)
{
	XLogSegNo	fc_lastRemovedSegNo;

	SpinLockAcquire(&XLogCtl->info_lck);
	fc_lastRemovedSegNo = XLogCtl->lastRemovedSegNo;
	SpinLockRelease(&XLogCtl->info_lck);

	return fc_lastRemovedSegNo;
}


/*
 * 更新共享内存中最后移除段号指针，以反映给定的XLOG文件已被移除。
 */
static void fc_UpdateLastRemovedPtr(char *fc_filename)
{
	uint32		fc_tli;
	XLogSegNo	fc_segno;

	XLogFromFileName(fc_filename, &fc_tli, &fc_segno, wal_segment_size);

	SpinLockAcquire(&XLogCtl->info_lck);
	if (fc_segno > XLogCtl->lastRemovedSegNo)
		XLogCtl->lastRemovedSegNo = fc_segno;
	SpinLockRelease(&XLogCtl->info_lck);
}

/*
 * 移除pg_wal中的所有临时日志文件
 *
 * 这在先前崩溃后的恢复开始时被调用，此时没有其他进程写入新的WAL数据。
 */
static void fc_RemoveTempXlogFiles(void)
{
	DIR		   *fc_xldir;
	struct dirent *fc_xlde;

	elog(DEBUG2, "removing all temporary WAL segments");

	fc_xldir = AllocateDir(XLOGDIR);
	while ((fc_xlde = ReadDir(fc_xldir, XLOGDIR)) != NULL)
	{
		char		fc_path[MAXPGPATH];

		if (strncmp(fc_xlde->d_name, "xlogtemp.", 9) != 0)
			continue;

		snprintf(fc_path, MAXPGPATH, XLOGDIR "/%s", fc_xlde->d_name);
		unlink(fc_path);
		elog(DEBUG2, "removed temporary WAL segment \"%s\"", fc_path);
	}
	FreeDir(fc_xldir);
}

/*
 * 回收或移除所有等于或早于传递的段号的日志文件。
 *
 * endptr是当前（或最近）xlog的末尾，lastredoptr是最后检查点的重做指针。这些用于确定我们是否要回收而不是删除不再需要的日志文件。
 *
 * insertTLI是XLOG插入的当前时间线。任何回收的段应为该时间线重复使用。
 */
static void fc_RemoveOldXlogFiles(XLogSegNo fc_segno, XLogRecPtr fc_lastredoptr, XLogRecPtr fc_endptr,
				   TimeLineID fc_insertTLI)
{
	DIR		   *fc_xldir;
	struct dirent *fc_xlde;
	char		fc_lastoff[MAXFNAMELEN];
	XLogSegNo	fc_endlogSegNo;
	XLogSegNo	fc_recycleSegNo;

	/* 初始化有关尝试回收目标的信息 */
	XLByteToSeg(fc_endptr, fc_endlogSegNo, wal_segment_size);
	fc_recycleSegNo = fc_XLOGfileslop(fc_lastredoptr);

	/*
	 * 构建要保留的最后段的文件名。时间线ID并不重要，我们在比较中忽略它。（在恢复期间，InsertTimeLineID未设置，因此我们无法使用它。）
	 */
	XLogFileName(fc_lastoff, 0, fc_segno, wal_segment_size);

	elog(DEBUG2, "attempting to remove WAL segments older than log file %s",
		 fc_lastoff);

	fc_xldir = AllocateDir(XLOGDIR);

	while ((fc_xlde = ReadDir(fc_xldir, XLOGDIR)) != NULL)
	{
		/* 忽略非XLOG段的文件 */
		if (!IsXLogFileName(fc_xlde->d_name) &&
			!IsPartialXLogFileName(fc_xlde->d_name))
			continue;

		/*
		 * 我们在决定段是否仍然需要时忽略XLOG段标识符的时间线部分。这确保我们不会过早地从父时间线中移除段。我们可能在移除非父时间线的段时可以更积极一些，但那将复杂得多。
		 *
		 * 我们使用文件名的字母数字排序属性来决定哪些段早于lastoff段。
		 */
		if (strcmp(fc_xlde->d_name + 8, fc_lastoff + 8) <= 0)
		{
			if (XLogArchiveCheckDone(fc_xlde->d_name))
			{
				/* 首先更新共享内存中的最后移除位置 */
				fc_UpdateLastRemovedPtr(fc_xlde->d_name);

				fc_RemoveXlogFile(fc_xlde->d_name, fc_recycleSegNo, &fc_endlogSegNo,
							   fc_insertTLI);
			}
		}
	}

	FreeDir(fc_xldir);
}

/*
 * 删除不属于给定时间线历史的WAL文件。
 *
 * 这将在恢复期间调用，每当我们切换到新的时间线时，以及在恢复结束时创建新的时间线时调用。我们本不会关心在pg_wal中存在的额外WAL文件，但它们可能是我们尚未使用过的旧时间线上预分配或回收的WAL段，并且包含垃圾。如果我们将它们留在pg_wal中，最终它们将被归档，而我们不能让这种情况发生。属于我们时间线历史的文件是有效的，因为我们已经成功重放过它们，但对于其他文件我们不能确定。
 *
 * 'switchpoint'是我们切换到新时间线时WAL中的当前点，'newTLI'是我们切换到的新时间线。
 */
void RemoveNonParentXlogFiles(XLogRecPtr fc_switchpoint, TimeLineID fc_newTLI)
{
	DIR		   *fc_xldir;
	struct dirent *fc_xlde;
	char		fc_switchseg[MAXFNAMELEN];
	XLogSegNo	fc_endLogSegNo;
	XLogSegNo	fc_switchLogSegNo;
	XLogSegNo	fc_recycleSegNo;

	/*
	 * 初始化关于从何开始工作的相关信息。这将回收，
	 * 有些随意，10个未来的段落。
	 */
	XLByteToPrevSeg(fc_switchpoint, fc_switchLogSegNo, wal_segment_size);
	XLByteToSeg(fc_switchpoint, fc_endLogSegNo, wal_segment_size);
	fc_recycleSegNo = fc_endLogSegNo + 10;

	/*
	 * 构造要保留的最后一个段的文件名。
	 */
	XLogFileName(fc_switchseg, fc_newTLI, fc_switchLogSegNo, wal_segment_size);

	elog(DEBUG2, "attempting to remove WAL segments newer than log file %s",
		 fc_switchseg);

	fc_xldir = AllocateDir(XLOGDIR);

	while ((fc_xlde = ReadDir(fc_xldir, XLOGDIR)) != NULL)
	{
		/* 忽略非XLOG段的文件 */
		if (!IsXLogFileName(fc_xlde->d_name))
			continue;

		/*
		 * 删除时间线比我们切换到的新时间线更旧的文件，但其段号 >= 新时间线的第一个段号。
		 */
		if (strncmp(fc_xlde->d_name, fc_switchseg, 8) < 0 &&
			strcmp(fc_xlde->d_name + 8, fc_switchseg + 8) > 0)
		{
			/*
			 * 然而，如果文件已经标记为.ready，则不要
			 * 立即删除它。删除它应该是可以的 - 不属于我们时间线历史的文件在恢复中不是必需的 - 但似乎更安全让它们被归档并在之后删除。
			 */
			if (!XLogArchiveIsReady(fc_xlde->d_name))
				fc_RemoveXlogFile(fc_xlde->d_name, fc_recycleSegNo, &fc_endLogSegNo,
							   fc_newTLI);
		}
	}

	FreeDir(fc_xldir);
}

/*
 * 回收或删除不再需要的日志文件。
 *
 * segname是要回收或删除的段的名称。recycleSegNo
 * 是要回收到的段号。endlogSegNo是当前（或最近）WAL的结束段号。
 *
 * 如果段被回收，则endlogSegNo会递增，以便将来调用此函数时不会再检查它。
 *
 * insertTLI是XLOG插入的当前时间线。任何回收的段
 * 应该用于此时间线。
 */
static void fc_RemoveXlogFile(const char *fc_segname, XLogSegNo fc_recycleSegNo,
			   XLogSegNo *fc_endlogSegNo, TimeLineID fc_insertTLI)
{
	char		fc_path[MAXPGPATH];
#ifdef WIN32
	char		newpath[MAXPGPATH];
#endif
	struct stat fc_statbuf;

	snprintf(fc_path, MAXPGPATH, XLOGDIR "/%s", fc_segname);

	/*
	 * 在删除文件之前，查看它是否可以被回收作为未来的日志
	 * 段。仅回收正常文件，因为我们不想回收指向单独归档目录的符号链接。
	 */
	if (wal_recycle &&
		*fc_endlogSegNo <= fc_recycleSegNo &&
		XLogCtl->InstallXLogFileSegmentActive &&	/* 调用者重新检查此项 */
		lstat(fc_path, &fc_statbuf) == 0 && S_ISREG(fc_statbuf.st_mode) &&
		fc_InstallXLogFileSegment(fc_endlogSegNo, fc_path,
							   true, fc_recycleSegNo, fc_insertTLI))
	{
		ereport(DEBUG2,
				(errmsg_internal("recycled write-ahead log file \"%s\"",
								 fc_segname)));
		CheckpointStats.ckpt_segs_recycled++;
		/* 将来迭代不需要重新检查该插槽 */
		(*fc_endlogSegNo)++;
	}
	else
	{
		/* 不需要更多未来段，或回收失败... */
		int			fc_rc;

		ereport(DEBUG2,
				(errmsg_internal("removing write-ahead log file \"%s\"",
								 fc_segname)));

#ifdef WIN32

		/*
		 * 在Windows上，如果另一个进程（例如另一个后端）以FILE_SHARE_DELETE模式保持文件打开，unlink将成功，但该文件仍会在目录列表中显示，直到最后一个句柄关闭。为了避免将悬挂的已删除文件与需要归档的实时WAL文件混淆，在删除之前重命名它。
		 *
		 * 如果另一个进程在没有FILE_SHARE_DELETE标志的情况下保持文件打开，重命名会失败。我们将在下一个检查点再试一次。
		 */
		snprintf(newpath, MAXPGPATH, "%s.deleted", path);
		if (rename(path, newpath) != 0)
		{
			ereport(LOG,
					(errcode_for_file_access(),
					 errmsg("could not rename file \"%s\": %m",
							path)));
			return;
		}
		fc_rc = durable_unlink(newpath, LOG);
#else
		fc_rc = durable_unlink(fc_path, LOG);
#endif
		if (fc_rc != 0)
		{
			/* 消息已经通过durable_unlink()记录 */
			return;
		}
		CheckpointStats.ckpt_segs_removed++;
	}

	XLogArchiveCleanup(fc_segname);
}

/*
 * 验证 pg_wal 和 pg_wal/archive_status 是否存在。
 * 如果后者不存在，则重新创建它。
 *
 * 这个函数的目标不是验证这些目录的内容，而是帮助处理那些
 * 在进行 PITR 目的的集群复制时遗漏了 pg_wal 的情况。
 *
 * 如果 pg_wal 不存在，我们也可以重新创建它，但做出了一个
 * 有意的政策决定不这样做。pg_wal 通常是一个符号链接，如果
 * 这是 DBA 的意图，那么自动创建一个普通目录将导致性能下降
 * 而没有任何通知。
 */
static void fc_ValidateXLOGDirectoryStructure(void)
{
	char		fc_path[MAXPGPATH];
	struct stat fc_stat_buf;

	/* 检查 pg_wal；如果不存在，则返回错误 */
	if (stat(XLOGDIR, &fc_stat_buf) != 0 ||
		!S_ISDIR(fc_stat_buf.st_mode))
		ereport(FATAL,
				(errmsg("required WAL directory \"%s\" does not exist",
						XLOGDIR)));

	/* 检查 archive_status */
	snprintf(fc_path, MAXPGPATH, XLOGDIR "/archive_status");
	if (stat(fc_path, &fc_stat_buf) == 0)
	{
		/* 检查存在但不是目录的异常情况 */
		if (!S_ISDIR(fc_stat_buf.st_mode))
			ereport(FATAL,
					(errmsg("required WAL directory \"%s\" does not exist",
							fc_path)));
	}
	else
	{
		ereport(LOG,
				(errmsg("creating missing WAL directory \"%s\"", fc_path)));
		if (MakePGDirectory(fc_path) < 0)
			ereport(FATAL,
					(errmsg("could not create missing directory \"%s\": %m",
							fc_path)));
	}
}

/*
 * 移除先前的备份历史文件。这也会重试创建
 * 对于之前 XLogArchiveNotify 失败的任何备份历史文件的
 * .ready 文件。
 */
static void fc_CleanupBackupHistory(void)
{
	DIR		   *fc_xldir;
	struct dirent *fc_xlde;
	char		fc_path[MAXPGPATH + sizeof(XLOGDIR)];

	fc_xldir = AllocateDir(XLOGDIR);

	while ((fc_xlde = ReadDir(fc_xldir, XLOGDIR)) != NULL)
	{
		if (IsBackupHistoryFileName(fc_xlde->d_name))
		{
			if (XLogArchiveCheckDone(fc_xlde->d_name))
			{
				elog(DEBUG2, "removing WAL backup history file \"%s\"",
					 fc_xlde->d_name);
				snprintf(fc_path, sizeof(fc_path), XLOGDIR "/%s", fc_xlde->d_name);
				unlink(fc_path);
				XLogArchiveCleanup(fc_xlde->d_name);
			}
		}
	}

	FreeDir(fc_xldir);
}

/*
 * pg_control 的 I/O 例程
 *
 * *ControlFile 是一个在共享内存中的缓冲区，保持 pg_control
 * 内容的图像。WriteControlFile() 在给定预加载缓冲区的情况下
 * 初始化 pg_control，ReadControlFile() 从 pg_control 文件加载缓冲区
 * （在 postmaster 或独立后台启动期间），而 UpdateControlFile()
 * 在我们修改 xlog 状态后重写 pg_control。InitControlFile() 用初始值
 * 填充缓冲区。
 *
 * 为了简单起见，WriteControlFile() 初始化与检查后台/数据库
 * 兼容性相关的 pg_control 字段，ReadControlFile() 验证它们是否正确。
 * 我们可以将 I/O 和兼容性检查功能分开，但目前似乎没有必要。
 */

static void fc_InitControlFile(uint64 fc_sysidentifier)
{
	char		fc_mock_auth_nonce[MOCK_AUTH_NONCE_LEN];

	/*
	 * 生成一个随机的 nonce。这用于因用户不存在而失败的
	 * 验证请求。nonce 用于为不存在的用户创建一个真
	 * 实的密码挑战，以替代实际存储的密码。
	 */
	if (!pg_strong_random(fc_mock_auth_nonce, MOCK_AUTH_NONCE_LEN))
		ereport(PANIC,
				(errcode(ERRCODE_INTERNAL_ERROR),
				 errmsg("could not generate secret authorization token")));

	memset(ControlFile, 0, sizeof(ControlFileData));
	/* 初始化 pg_control 状态字段 */
	ControlFile->system_identifier = fc_sysidentifier;
	memcpy(ControlFile->mock_authentication_nonce, fc_mock_auth_nonce, MOCK_AUTH_NONCE_LEN);
	ControlFile->state = DB_SHUTDOWNED;
	ControlFile->unloggedLSN = FirstNormalUnloggedLSN;

	/* 设置在重放 WAL 时使用的重要参数值 */
	ControlFile->MaxConnections = MaxConnections;
	ControlFile->max_worker_processes = max_worker_processes;
	ControlFile->max_wal_senders = max_wal_senders;
	ControlFile->max_prepared_xacts = max_prepared_xacts;
	ControlFile->max_locks_per_xact = max_locks_per_xact;
	ControlFile->wal_level = wal_level;
	ControlFile->wal_log_hints = wal_log_hints;
	ControlFile->track_commit_timestamp = track_commit_timestamp;
	ControlFile->data_checksum_version = bootstrap_data_checksum_version;
}

static void fc_WriteControlFile(void)
{
	int			fc_fd;
	char		fc_buffer[PG_CONTROL_FILE_SIZE];	/* 不必对齐 */

	/*
	 * 确保 pg_control 数据结构的大小合理。请参阅
	 * pg_control.h 中这些符号的注释。
	 */
	StaticAssertStmt(sizeof(ControlFileData) <= PG_CONTROL_MAX_SAFE_SIZE,
					 "pg_control is too large for atomic disk writes");
	StaticAssertStmt(sizeof(ControlFileData) <= PG_CONTROL_FILE_SIZE,
					 "sizeof(ControlFileData) exceeds PG_CONTROL_FILE_SIZE");

	/*
	 * 初始化版本和兼容性检查字段
	 */
	ControlFile->pg_control_version = PG_CONTROL_VERSION;
	ControlFile->catalog_version_no = CATALOG_VERSION_NO;

	ControlFile->maxAlign = MAXIMUM_ALIGNOF;
	ControlFile->floatFormat = FLOATFORMAT_VALUE;

	ControlFile->blcksz = BLCKSZ;
	ControlFile->relseg_size = RELSEG_SIZE;
	ControlFile->xlog_blcksz = XLOG_BLCKSZ;
	ControlFile->xlog_seg_size = wal_segment_size;

	ControlFile->nameDataLen = NAMEDATALEN;
	ControlFile->indexMaxKeys = INDEX_MAX_KEYS;

	ControlFile->toast_max_chunk_size = TOAST_MAX_CHUNK_SIZE;
	ControlFile->loblksize = LOBLKSIZE;

	ControlFile->float8ByVal = FLOAT8PASSBYVAL;

	/* 内容通过 CRC 保护 */
	INIT_CRC32C(ControlFile->crc);
	COMP_CRC32C(ControlFile->crc,
				(char *) ControlFile,
				offsetof(ControlFileData, crc));
	FIN_CRC32C(ControlFile->crc);

	/*
	 * 我们在 pg_control 中写入 PG_CONTROL_FILE_SIZE 字节，零填充
	 * 超过 sizeof(ControlFileData) 的部分。这减少了阅读 pg_control 时
	 * 遇到 premature-EOF 错误的几率。我们在检查文件内容时仍然会失败，
	 * 但希望能得到一个比“无法读取 pg_control”更具体的错误。
	 */
	memset(fc_buffer, 0, PG_CONTROL_FILE_SIZE);
	memcpy(fc_buffer, ControlFile, sizeof(ControlFileData));

	fc_fd = BasicOpenFile(XLOG_CONTROL_FILE,
					   O_RDWR | O_CREAT | O_EXCL | PG_BINARY);
	if (fc_fd < 0)
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not create file \"%s\": %m",
						XLOG_CONTROL_FILE)));

	errno = 0;
	pgstat_report_wait_start(WAIT_EVENT_CONTROL_FILE_WRITE);
	if (write(fc_fd, fc_buffer, PG_CONTROL_FILE_SIZE) != PG_CONTROL_FILE_SIZE)
	{
		/* 如果写入未设置 errno，假设问题是没有磁盘空间 */
		if (errno == 0)
			errno = ENOSPC;
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not write to file \"%s\": %m",
						XLOG_CONTROL_FILE)));
	}
	pgstat_report_wait_end();

	pgstat_report_wait_start(WAIT_EVENT_CONTROL_FILE_SYNC);
	if (pg_fsync(fc_fd) != 0)
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not fsync file \"%s\": %m",
						XLOG_CONTROL_FILE)));
	pgstat_report_wait_end();

	if (close(fc_fd) != 0)
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m",
						XLOG_CONTROL_FILE)));
}

static void fc_ReadControlFile(void)
{
	pg_crc32c	fc_crc;
	int			fc_fd;
	static char fc_wal_segsz_str[20];
	int			fc_r;

	/*
	 * 读取数据...
	 */
	fc_fd = BasicOpenFile(XLOG_CONTROL_FILE,
					   O_RDWR | PG_BINARY);
	if (fc_fd < 0)
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\": %m",
						XLOG_CONTROL_FILE)));

	pgstat_report_wait_start(WAIT_EVENT_CONTROL_FILE_READ);
	fc_r = read(fc_fd, ControlFile, sizeof(ControlFileData));
	if (fc_r != sizeof(ControlFileData))
	{
		if (fc_r < 0)
			ereport(PANIC,
					(errcode_for_file_access(),
					 errmsg("could not read file \"%s\": %m",
							XLOG_CONTROL_FILE)));
		else
			ereport(PANIC,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("could not read file \"%s\": read %d of %zu",
							XLOG_CONTROL_FILE, fc_r, sizeof(ControlFileData))));
	}
	pgstat_report_wait_end();

	close(fc_fd);

	/*
	 * 检查预期的 pg_control 格式版本。如果这个版本错误，  
	 * CRC 检查很可能会失败，因为我们将检查错误的字节数。  
	 * 报告错误版本可能比报告错误的 CRC 更有意义。  
	 */

	if (ControlFile->pg_control_version != PG_CONTROL_VERSION && ControlFile->pg_control_version % 65536 == 0 && ControlFile->pg_control_version / 65536 != 0)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with PG_CONTROL_VERSION %d (0x%08x),"
						   " but the server was compiled with PG_CONTROL_VERSION %d (0x%08x).",
						   ControlFile->pg_control_version, ControlFile->pg_control_version,
						   PG_CONTROL_VERSION, PG_CONTROL_VERSION),
				 errhint("This could be a problem of mismatched byte ordering.  It looks like you need to initdb.")));

	if (ControlFile->pg_control_version != PG_CONTROL_VERSION)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with PG_CONTROL_VERSION %d,"
						   " but the server was compiled with PG_CONTROL_VERSION %d.",
						   ControlFile->pg_control_version, PG_CONTROL_VERSION),
				 errhint("It looks like you need to initdb.")));

	/* 现在检查 CRC。 */
	INIT_CRC32C(fc_crc);
	COMP_CRC32C(fc_crc,
				(char *) ControlFile,
				offsetof(ControlFileData, crc));
	FIN_CRC32C(fc_crc);

	if (!EQ_CRC32C(fc_crc, ControlFile->crc))
		ereport(FATAL,
				(errmsg("incorrect checksum in control file")));

	/*
	 * 立即进行兼容性检查。如果数据库与后端可执行文件不兼容，  
	 * 我们希望在可能造成任何损害之前中止。  
	 */
	if (ControlFile->catalog_version_no != CATALOG_VERSION_NO)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with CATALOG_VERSION_NO %d,"
						   " but the server was compiled with CATALOG_VERSION_NO %d.",
						   ControlFile->catalog_version_no, CATALOG_VERSION_NO),
				 errhint("It looks like you need to initdb.")));
	if (ControlFile->maxAlign != MAXIMUM_ALIGNOF)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with MAXALIGN %d,"
						   " but the server was compiled with MAXALIGN %d.",
						   ControlFile->maxAlign, MAXIMUM_ALIGNOF),
				 errhint("It looks like you need to initdb.")));
	if (ControlFile->floatFormat != FLOATFORMAT_VALUE)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster appears to use a different floating-point number format than the server executable."),
				 errhint("It looks like you need to initdb.")));
	if (ControlFile->blcksz != BLCKSZ)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with BLCKSZ %d,"
						   " but the server was compiled with BLCKSZ %d.",
						   ControlFile->blcksz, BLCKSZ),
				 errhint("It looks like you need to recompile or initdb.")));
	if (ControlFile->relseg_size != RELSEG_SIZE)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with RELSEG_SIZE %d,"
						   " but the server was compiled with RELSEG_SIZE %d.",
						   ControlFile->relseg_size, RELSEG_SIZE),
				 errhint("It looks like you need to recompile or initdb.")));
	if (ControlFile->xlog_blcksz != XLOG_BLCKSZ)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with XLOG_BLCKSZ %d,"
						   " but the server was compiled with XLOG_BLCKSZ %d.",
						   ControlFile->xlog_blcksz, XLOG_BLCKSZ),
				 errhint("It looks like you need to recompile or initdb.")));
	if (ControlFile->nameDataLen != NAMEDATALEN)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with NAMEDATALEN %d,"
						   " but the server was compiled with NAMEDATALEN %d.",
						   ControlFile->nameDataLen, NAMEDATALEN),
				 errhint("It looks like you need to recompile or initdb.")));
	if (ControlFile->indexMaxKeys != INDEX_MAX_KEYS)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with INDEX_MAX_KEYS %d,"
						   " but the server was compiled with INDEX_MAX_KEYS %d.",
						   ControlFile->indexMaxKeys, INDEX_MAX_KEYS),
				 errhint("It looks like you need to recompile or initdb.")));
	if (ControlFile->toast_max_chunk_size != TOAST_MAX_CHUNK_SIZE)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with TOAST_MAX_CHUNK_SIZE %d,"
						   " but the server was compiled with TOAST_MAX_CHUNK_SIZE %d.",
						   ControlFile->toast_max_chunk_size, (int) TOAST_MAX_CHUNK_SIZE),
				 errhint("It looks like you need to recompile or initdb.")));
	if (ControlFile->loblksize != LOBLKSIZE)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with LOBLKSIZE %d,"
						   " but the server was compiled with LOBLKSIZE %d.",
						   ControlFile->loblksize, (int) LOBLKSIZE),
				 errhint("It looks like you need to recompile or initdb.")));

#ifdef USE_FLOAT8_BYVAL
	if (ControlFile->float8ByVal != true)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized without USE_FLOAT8_BYVAL"
						   " but the server was compiled with USE_FLOAT8_BYVAL."),
				 errhint("It looks like you need to recompile or initdb.")));
#else
	if (ControlFile->float8ByVal != false)
		ereport(FATAL,
				(errmsg("database files are incompatible with server"),
				 errdetail("The database cluster was initialized with USE_FLOAT8_BYVAL"
						   " but the server was compiled without USE_FLOAT8_BYVAL."),
				 errhint("It looks like you need to recompile or initdb.")));
#endif

	wal_segment_size = ControlFile->xlog_seg_size;

	if (!IsValidWalSegSize(wal_segment_size))
		ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						errmsg_plural("WAL segment size must be a power of two between 1 MB and 1 GB, but the control file specifies %d byte",
									  "WAL segment size must be a power of two between 1 MB and 1 GB, but the control file specifies %d bytes",
									  wal_segment_size,
									  wal_segment_size)));

	snprintf(fc_wal_segsz_str, sizeof(fc_wal_segsz_str), "%d", wal_segment_size);
	SetConfigOption("wal_segment_size", fc_wal_segsz_str, PGC_INTERNAL,
					PGC_S_DYNAMIC_DEFAULT);

	/* 检查并更新依赖于 wal_segment_size 的变量 */
	if (ConvertToXSegs(min_wal_size_mb, wal_segment_size) < 2)
		ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						errmsg("\"min_wal_size\" must be at least twice \"wal_segment_size\"")));

	if (ConvertToXSegs(max_wal_size_mb, wal_segment_size) < 2)
		ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						errmsg("\"max_wal_size\" must be at least twice \"wal_segment_size\"")));

	UsableBytesInSegment =
		(wal_segment_size / XLOG_BLCKSZ * UsableBytesInPage) -
		(SizeOfXLogLongPHD - SizeOfXLogShortPHD);

	fc_CalculateCheckpointSegments();

	/* 使 initdb 设置也作为 GUC 变量可见 */
	SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
					PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
}

/*
 * 更新控制文件的工具包装器。请注意，控制文件会被刷新。
 */
static void fc_UpdateControlFile(void)
{
	update_controlfile(DataDir, ControlFile, true);
}

#ifdef FDD
#define FDB_HASH_PASSWD_LEN (32*2 +3)
//获取 hash 后的外部密钥
//只有在中途开启透明加密时，才设置 is_check_both 为 true，
//用于在手动输入密码情况下，如果发现也配置了密钥配置文件，则比较两者 hash 后的密码是否一致, 只有一致才允许开启透明加密
static bool fc_is_tde_enable(unsigned char *fc_Extkey, bool fc_is_check_both)
{
	//如果手动输入密码
	if( NULL != g_tdepass)
	{		
		if( FDB_HASH_PASSWD_LEN != strlen(g_tdepass))
		{
			ereport(PANIC,(errcode(ERRCODE_WARNING), errmsg("g_tdepass length is invalid: %s",g_tdepass)));
	 	}
		if( 3 != strlen(fdb_encrypttype_tochar(g_tde_encrypt)))
		{
			ereport(PANIC,(errcode(ERRCODE_WARNING), errmsg("g_tde_encrypt length is invalid: %s",fdb_encrypttype_tochar(g_tde_encrypt))));
	 	}
		if (strncmp(g_tdepass, fdb_encrypttype_tochar(g_tde_encrypt), 3) != 0)
		{
			char fc_tde_type[3 + 1]={0};
			strncpy(fc_tde_type, g_tdepass, 3);
			fc_tde_type[3] = 0;
			ereport(PANIC,(errcode(ERRCODE_WARNING),
				errmsg("Differences between the input TDE encrypt type(%s) and tde_encrypt(%s) in  postgresql configure!",fc_tde_type,fdb_encrypttype_tochar(g_tde_encrypt))));
		}
		if (strspn(g_tdepass + 3, MD5_PASSWD_CHARSET) != FDB_HASH_PASSWD_LEN - 3)
		{
			ereport(PANIC,(errcode(ERRCODE_WARNING), errmsg("g_tdepass(%s) Incorrect data type",g_tdepass)));
		}
		HexTobytes(g_tdepass + 3,FDB_HASH_PASSWD_LEN - 3,fc_Extkey,FDB_EXTKEY_LEN);

		if(fc_is_check_both && NULL != g_fdbEncryptionKeyCommand)
		{		
			unsigned char fc_file_Extkey[FDB_EXTKEY_LEN] = {0};
			if( strlen(g_fdbEncryptionKeyCommand) == strlen("input") && strncmp(g_fdbEncryptionKeyCommand, "input",strlen("input")) == 0)
			{
				return true;
			}
			
			fdb_getPageEncryptKey(g_fdbEncryptionKeyCommand, fc_file_Extkey);
			if(strncmp((char *)fc_Extkey,(char *)fc_file_Extkey,FDB_EXTKEY_LEN) != 0)
			{
				ereport(PANIC,(errcode(ERRCODE_WARNING), errmsg("The manually entered TDE key does not match the key configured in g_fdbEncryptionKeyCommand")));
			}
		}
		
		return true;
	}
	else if(NULL != g_fdbEncryptionKeyCommand && FDB_ENCRYPT_TYPE_NONE != g_tde_encrypt)
	{
		if( strlen(g_fdbEncryptionKeyCommand) == strlen("input") && strncmp(g_fdbEncryptionKeyCommand, "input",strlen("input")) == 0)
		{
			ereport(PANIC,(errcode(ERRCODE_WARNING), errmsg("g_fdbEncryptionKeyCommand is : %s, so need manually enter password","input")));
		}
		fdb_getPageEncryptKey(g_fdbEncryptionKeyCommand, fc_Extkey);
		return true;
	}
	
	return false;
}

//初始化 TDE 的秘钥
static void fc_fdb_checkAndInitTdeEncrypt(void)
{
	unsigned char fc_Extkey[FDB_EXTKEY_LEN];	

	if(ControlFile->encrypted != FDB_ENCRYPT_TYPE_NONE && NULL == g_fdbEncryptionKeyCommand)
	{
        ereport(PANIC,
            (errcode(ERRCODE_WARNING),
                errmsg("Database instance is encrypted. but not found encryption_key_command in postgresql configure\n")));
	}	
	
	if( ControlFile->encrypted == FDB_ENCRYPT_TYPE_NONE && NULL != g_fdbEncryptionKeyCommand && FDB_ENCRYPT_TYPE_NONE != g_tde_encrypt)
	{   //中途开启了TDE功能功能  
		fc_is_tde_enable(fc_Extkey, true);		
		ereport(LOG,
           	 (errcode(ERRCODE_WARNING),
                errmsg("The Transparent Data Encryption (TDE) has been enabled with the %s encryption algorithm.\n",
                fdb_encrypttype_tochar(g_tde_encrypt))));			

		//初试化tde加密密钥并且给ControlFile->tde_encrypt_tablekey赋值，所用需要提前调用      
		fdb_initPageEncrypt(fc_Extkey, ControlFile->system_identifier, ControlFile->tde_encrypt_tablekey, g_tde_encrypt, true);
		ControlFile->encrypted = g_tde_encrypt;		
		ControlFile->chkseckey = fdb_getPageEncryptChkKey(fc_Extkey, ControlFile->system_identifier, ControlFile->tde_encrypt_tablekey);
		
		update_controlfile(DataDir, ControlFile, true);		
		return;      
	}
	
	if(ControlFile->encrypted == FDB_ENCRYPT_TYPE_NONE)
	{
		if( NULL != g_tdepass)//如果启动时输入了TDE密码，但是又没有开启TDE，则提示忽略密码  
		{
		   ereport(LOG,
            (errcode(ERRCODE_WARNING),
                errmsg("TDE encryption is not enabled for the database; So ignore the entered password\n")));
		}
		return;//如果之前没有开启TDE，当前也没有开启，则直接退出  
	}
	
	fc_is_tde_enable(fc_Extkey, true);
	if(ControlFile->encrypted != g_tde_encrypt && FDB_ENCRYPT_TYPE_NONE != g_tde_encrypt)
	{
		//如果中途变更TDE加密算法，如果没有开启强制变更配置，则直接报错，数据库中断运行  
#ifdef FDDGM
		int fc_log_level = g_fdballowChangeTdeEncryptType ? LOG: PANIC;
#else
		int fc_log_level = PANIC;
#endif
		ereport(fc_log_level,
            (errcode(ERRCODE_WARNING),
                errmsg("The previously TDE encryption was %s, and the current TDE encryption change to %s. This change may potentially lead to anomalies or issues.\n",
                fdb_encrypttype_tochar(ControlFile->encrypted),fdb_encrypttype_tochar(g_tde_encrypt))));

		//中途变更TDE加密算法并且开启了允许强制变更配置，该场景可能存在说句异常，慎重操作  
		//初试化tde加密密钥并且给ControlFile->tde_encrypt_tablekey赋值，所用需要提前调用      
		fdb_initPageEncrypt(fc_Extkey, ControlFile->system_identifier, ControlFile->tde_encrypt_tablekey, g_tde_encrypt, true);
		ControlFile->encrypted = g_tde_encrypt;
		ControlFile->chkseckey = fdb_getPageEncryptChkKey(fc_Extkey, ControlFile->system_identifier,ControlFile->tde_encrypt_tablekey);
		
		update_controlfile(DataDir, ControlFile, true);		
		return;  
	}
	
	if(ControlFile->encrypted != g_tde_encrypt)
	{
	    //之前开启了透明加密，但是当前时刻取消了加密，则打印提示
		ereport(LOG,
           	 (errcode(ERRCODE_WARNING),
                errmsg("Previously enabled transparent encryption(TDE) with the %s algorithm, but now disabled encryption(%s).\n",
                fdb_encrypttype_tochar(ControlFile->encrypted),fdb_encrypttype_tochar(g_tde_encrypt))));
	}
	//fdb_getPageEncryptKey(g_fdbEncryptionKeyCommand, Extkey);  
	if(fdb_checkPageEncryptSecKey(fc_Extkey, ControlFile->system_identifier, ControlFile->tde_encrypt_tablekey, ControlFile->chkseckey) != 0)
	{
        ereport(PANIC,
            (errcode(ERRCODE_WARNING),
                errmsg("Database security key not correct.\n")));
	}
	fdb_initPageEncrypt(fc_Extkey, ControlFile->system_identifier, ControlFile->tde_encrypt_tablekey, ControlFile->encrypted, false);
}

//初始化 TDE 的秘钥
void fdb_checkAndInitEncrypt(void)
{
#ifdef USE_SSL
	bool fc_is_create_rown = false;
	ResourceOwner fc_resowner;

	if(CurrentResourceOwner == NULL)
	{
		CurrentResourceOwner = ResourceOwnerCreate(NULL, "checkAndInitEncrypt");
		fc_is_create_rown = true;
	}
#endif
	fc_fdb_checkAndInitTdeEncrypt();

#ifdef USE_SSL
	if(fc_is_create_rown && CurrentResourceOwner != NULL)
	{
		fc_resowner = CurrentResourceOwner;
		CurrentResourceOwner = NULL;

		/* 现在我们可以释放资源并删除它。 */
		ResourceOwnerRelease(fc_resowner,
							 RESOURCE_RELEASE_BEFORE_LOCKS, false, true);
		ResourceOwnerRelease(fc_resowner,
							 RESOURCE_RELEASE_LOCKS, false, true);
		ResourceOwnerRelease(fc_resowner,
							 RESOURCE_RELEASE_AFTER_LOCKS, false, true);
		ResourceOwnerDelete(fc_resowner);
	}
#endif

}


//更新用于加密TDE密钥的外部主密钥  
void fdb_change_tde_passwd(char *fc_old_pass, char *fc_new_pass)
{
	const char *fc_errstr = NULL;
	unsigned char fc_old_Extkey[FDB_EXTKEY_LEN] = {0};
	unsigned char fc_new_Extkey[FDB_EXTKEY_LEN] = {0};
	int fc_old_pass_len =  strlen(fc_old_pass);
	int fc_new_pass_len =  strlen(fc_new_pass);
#ifdef FDDGM
	pg_cryptohash_type fc_hash_type = PG_SM3;
#else
	pg_cryptohash_type fc_hash_type = PG_SHA256;
#endif
	if(ControlFile->encrypted == FDB_ENCRYPT_TYPE_NONE)
	{
		elog(ERROR, "TDE encryption is not enabled for the database; So ignore the change password.");
	}

    //校验旧密码是否正确
	//将旧密码进行摘要计算
	if(fdb_cryptohash_binary((unsigned char*)fc_old_pass, fc_old_pass_len, fc_hash_type, fc_old_Extkey, &fc_errstr) < 0)
    {       
	    elog(ERROR, "Failed to get old_pass hash(%s) .",fc_errstr);   
    }
	//校验旧密码是否是正确输入的TDE外部主密钥  
	if(fdb_checkPageEncryptSecKey(fc_old_Extkey, ControlFile->system_identifier, ControlFile->tde_encrypt_tablekey, ControlFile->chkseckey) != 0)
	{
		elog(ERROR, "The input 'old_pass' does not match the previously set password.");
	}

	//设置新密码
	//将新密码进行摘要计算
	if(fdb_cryptohash_binary((unsigned char*)fc_new_pass, fc_new_pass_len, fc_hash_type, fc_new_Extkey, &fc_errstr) < 0)
    {       
	    elog(ERROR, "Failed to get new_pass hash(%s) .",fc_errstr);   
    }
	//用旧密码解出对象密钥，然后用新密码重新加密对象密钥，将加密后的密钥更新到 ControlFile->tde_encrypt_tablekey 中  
	fdb_updatePageEncrypt(fc_old_Extkey, fc_new_Extkey, ControlFile->system_identifier, ControlFile->tde_encrypt_tablekey, g_tde_encrypt);		
	ControlFile->chkseckey = fdb_getPageEncryptChkKey(fc_new_Extkey, ControlFile->system_identifier, ControlFile->tde_encrypt_tablekey);
	
	update_controlfile(DataDir, ControlFile, true);	
	return;

}

//用于BootstrapModeMain进程检测是否开启TDE，并且将相关加密密钥赋值到ControlFile中保存  
//必须要从BootStrapXLOG 中移出，因为在调用BootStrapXLOG时，还没有给CurrentResourceOwner赋值，使得没法在开启OpenSSL时获取摘要  
void fdb_BootstrapModeMain_set_tde(void)
{
	unsigned char fc_Extkey[32] = {0};
	Assert(FDB_ENCRYPT_TYPE_NONE == ControlFile->encrypted);
	if(FDB_ENCRYPT_TYPE_NONE == ControlFile->encrypted && fc_is_tde_enable(fc_Extkey, false))
	{
		//初试化tde加密密钥并且给ControlFile->tde_encrypt_tablekey赋值，所用需要提前调用      
		fdb_initPageEncrypt(fc_Extkey, ControlFile->system_identifier, ControlFile->tde_encrypt_tablekey ,g_tde_encrypt, true);
		ControlFile->encrypted = g_tde_encrypt;
		ControlFile->chkseckey = fdb_getPageEncryptChkKey(fc_Extkey, ControlFile->system_identifier, ControlFile->tde_encrypt_tablekey);
		update_controlfile(DataDir, ControlFile, true);	
	}
	return;
}
#endif //FDD
/*
 * 从控制文件返回唯一的系统标识符。
 */
uint64 GetSystemIdentifier(void)
{
	Assert(ControlFile != NULL);
	return ControlFile->system_identifier;
}

/*
 * 从控制文件返回随机 nonce。
 */
char * GetMockAuthenticationNonce(void)
{
	Assert(ControlFile != NULL);
	return ControlFile->mock_authentication_nonce;
}

/*
 * 数据页是否启用了校验和？
 */
bool DataChecksumsEnabled(void)
{
	Assert(ControlFile != NULL);
	return (ControlFile->data_checksum_version > 0);
}

/*
 * 为未记录的关系返回一个虚假的 LSN。
 *
 * 每次调用生成一个大于之前返回的任何值的 LSN  
 * 当前计数器值会在干净关闭期间保存和恢复，但像未记录的关系一样，  
 * 不会在崩溃时保留。这可以在需要 LSN 类似的递增序列号而  
 * 不写任何 WAL 时使用，替代 XLogInsert 返回的真实 LSN 值。 
 */
XLogRecPtr GetFakeLSNForUnloggedRel(void)
{
	XLogRecPtr	fc_nextUnloggedLSN;

	/* 增加 unloggedLSN 计数器，需要 SpinLock */
	SpinLockAcquire(&XLogCtl->ulsn_lck);
	fc_nextUnloggedLSN = XLogCtl->unloggedLSN++;
	SpinLockRelease(&XLogCtl->ulsn_lck);

	return fc_nextUnloggedLSN;
}

/*
 * 自动调整 XLOG 缓冲区的数量。
 *
 * wal_buffers 的首选设置约为 shared_buffers 的 3%，  
 * 最大为一个 XLOG 段（至少在我们强制在切换日志文件时  
 * 执行 fsync 的情况下，几乎没有理由认为更多是有帮助的），  
 * 最小为 8 个块（这是 PostgreSQL 9.1 之前的默认值，  
 * 当时添加了自动调整功能）。
 *
 * 在 NBuffers 收到最终值之前，不应调用此函数。
 */
static int fc_XLOGChooseNumBuffers(void)
{
	int			fc_xbuffers;

	fc_xbuffers = NBuffers / 32;
	if (fc_xbuffers > (wal_segment_size / XLOG_BLCKSZ))
		fc_xbuffers = (wal_segment_size / XLOG_BLCKSZ);
	if (fc_xbuffers < 8)
		fc_xbuffers = 8;
	return fc_xbuffers;
}

/*
 * wal_buffers 的 GUC check_hook
 */
bool check_wal_buffers(int *fc_newval, void **fc_extra, GucSource fc_source)
{
	/*
	 * -1 表示请求自动调整。
	 */
	if (*fc_newval == -1)
	{
		/*
		 * 如果我们尚未更改 boot_val 的默认值 -1，  
		 * 就让它保持原样。我们将在调用 XLOGShmemSize 时修复它。
		 */
		if (XLOGbuffers == -1)
			return true;

		/* 否则，替换为自动调整值 */
		*fc_newval = fc_XLOGChooseNumBuffers();
	}

	/*
	 * 我们手动将设置的值限制为至少 4 个块。  
	 * 在 PostgreSQL 9.1 之前，gu.c 强制执行最小值为 4，  
	 * 但由于这种情况不再存在，  
	 * 我们只是默默地将这些值视为对最小值的请求。  
	 * （我们可以抛出错误，但那似乎不是很有帮助。） 
	 */
	if (*fc_newval < 4)
		*fc_newval = 4;

	return true;
}

/*
 * 读取控制文件，设置相应的 GUC。
 *
 * 这在启动期间调用，包括崩溃恢复周期，除非在引导模式下，此时尚不存在控制文件。由于还没有可用的共享内存（其大小可能依赖于控制文件的内容！），首先将内容存储在本地内存中。XLOGShmemInit()
 * 将稍后将其复制到共享内存中。
 *
 * reset 仅控制是否期望之前的内容（在重置情况下，有一个悬空指针指向旧的共享内存），或者不期望。
 */
void LocalProcessControlFile(bool fc_reset)
{
	Assert(fc_reset || ControlFile == NULL);
	ControlFile = palloc(sizeof(ControlFileData));
	fc_ReadControlFile();
}

/*
 * XLOG 的共享内存初始化
 */
Size XLOGShmemSize(void)
{
	Size		fc_size;

	/*
	 * 如果 wal_buffers 的值为 -1，使用首选的自动调优值。
	 * 这不是一个非常干净的地方，但我们必须等到 NBuffers 收到最终值，并且必须在使用 XLOGbuffers 的值进行任何重要操作之前进行。
	 *
	 * 我们更倾向于将此值的来源报告为 PGC_S_DYNAMIC_DEFAULT。
	 * 但是，如果 DBA 在配置文件中明确设置 wal_buffers = -1，
	 * 则 PGC_S_DYNAMIC_DEFAULT 将无法覆盖该值，我们必须使用 PGC_S_OVERRIDE 强制处理。
	 */
	if (XLOGbuffers == -1)
	{
		char		fc_buf[32];

		snprintf(fc_buf, sizeof(fc_buf), "%d", fc_XLOGChooseNumBuffers());
		SetConfigOption("wal_buffers", fc_buf, PGC_POSTMASTER,
						PGC_S_DYNAMIC_DEFAULT);
		if (XLOGbuffers == -1)	/* 应用失败？ */
			SetConfigOption("wal_buffers", fc_buf, PGC_POSTMASTER,
							PGC_S_OVERRIDE);
	}
	Assert(XLOGbuffers > 0);

	/* XLogCtl */
	fc_size = sizeof(XLogCtlData);

	/* WAL 插入锁，以及对齐 */
	fc_size = add_size(fc_size, mul_size(sizeof(WALInsertLockPadded), NUM_XLOGINSERT_LOCKS + 1));
	/* xlblocks 数组 */
	fc_size = add_size(fc_size, mul_size(sizeof(XLogRecPtr), XLOGbuffers));
	/* XLOG I/O 缓冲区的额外对齐填充 */
	fc_size = add_size(fc_size, XLOG_BLCKSZ);
	/* 以及缓冲区本身 */
	fc_size = add_size(fc_size, mul_size(XLOG_BLCKSZ, XLOGbuffers));

	/*
	 * 注意：我们不计算 ControlFileData，它来自 CreateSharedMemoryAndSemaphores 添加的“剩余因子”。
	 * 这允许我们在下面再次使用此例程来计算实际的分配大小。
	 */

	return fc_size;
}

void XLOGShmemInit(void)
{
	bool		fc_foundCFile,
				fc_foundXLog;
	char	   *fc_allocptr;
	int			fc_i;
	ControlFileData *fc_localControlFile;

#ifdef WAL_DEBUG

	/*
	 * 创建一个用于 WAL 调试的内存上下文，免于正常
	 * “关键区域内不进行 palloc”的规则。是的，分配失败可能导致 PANIC，
	 * 但 wal_debug 反正不适合生产使用。
	 */
	if (walDebugCxt == NULL)
	{
		walDebugCxt = AllocSetContextCreate(TopMemoryContext,
											"WAL Debug",
											ALLOCSET_DEFAULT_SIZES);
		MemoryContextAllowInCriticalSection(walDebugCxt, true);
	}
#endif


	XLogCtl = (XLogCtlData *)
		ShmemInitStruct("XLOG Ctl", XLOGShmemSize(), &fc_foundXLog);

	fc_localControlFile = ControlFile;
	ControlFile = (ControlFileData *)
		ShmemInitStruct("Control File", sizeof(ControlFileData), &fc_foundCFile);

	if (fc_foundCFile || fc_foundXLog)
	{
		/* 两者应该同时存在或都不存在 */
		Assert(fc_foundCFile && fc_foundXLog);

		/* 初始化 WALInsertLocks 的本地副本 */
		WALInsertLocks = XLogCtl->Insert.WALInsertLocks;

		if (fc_localControlFile)
			pfree(fc_localControlFile);
		return;
	}
	memset(XLogCtl, 0, sizeof(XLogCtlData));

	/*
	 * 除非在引导模式下，否则已经本地读取控制文件。将内容移动
	 * 到共享内存中。
	 */
	if (fc_localControlFile)
	{
		memcpy(ControlFile, fc_localControlFile, sizeof(ControlFileData));
		pfree(fc_localControlFile);
	}

	/*
	 * 由于 XLogCtlData 包含 XLogRecPtr 字段，因此其大小应该是
	 * 同样的对齐方式的倍数，因此这里不需要额外的对齐填充。
	 */
	fc_allocptr = ((char *) XLogCtl) + sizeof(XLogCtlData);
	XLogCtl->xlblocks = (XLogRecPtr *) fc_allocptr;
	memset(XLogCtl->xlblocks, 0, sizeof(XLogRecPtr) * XLOGbuffers);
	fc_allocptr += sizeof(XLogRecPtr) * XLOGbuffers;


	/* WAL 插入锁。确保它们与完整的填充大小对齐 */
	fc_allocptr += sizeof(WALInsertLockPadded) -
		((uintptr_t) fc_allocptr) % sizeof(WALInsertLockPadded);
	WALInsertLocks = XLogCtl->Insert.WALInsertLocks =
		(WALInsertLockPadded *) fc_allocptr;
	fc_allocptr += sizeof(WALInsertLockPadded) * NUM_XLOGINSERT_LOCKS;

	for (fc_i = 0; fc_i < NUM_XLOGINSERT_LOCKS; fc_i++)
	{
		LWLockInitialize(&WALInsertLocks[fc_i].l.lock, LWTRANCHE_WAL_INSERT);
		WALInsertLocks[fc_i].l.insertingAt = InvalidXLogRecPtr;
		WALInsertLocks[fc_i].l.lastImportantAt = InvalidXLogRecPtr;
	}

	/*
	 * 将页面缓冲区的起始位置对齐到完整的 xlog 块大小边界。
	 * 这简化了 XLOG 插入中的一些计算。这对于 O_DIRECT 也是必需的。
	 */
	fc_allocptr = (char *) TYPEALIGN(XLOG_BLCKSZ, fc_allocptr);
	XLogCtl->pages = fc_allocptr;
	memset(XLogCtl->pages, 0, (Size) XLOG_BLCKSZ * XLOGbuffers);

	/*
	 * 执行 XLogCtl 共享数据的基本初始化。 (StartupXLOG 将填写
	 * 额外的信息。)
	 */
	XLogCtl->XLogCacheBlck = XLOGbuffers - 1;
	XLogCtl->SharedRecoveryState = RECOVERY_STATE_CRASH;
	XLogCtl->InstallXLogFileSegmentActive = false;
	XLogCtl->WalWriterSleeping = false;

	SpinLockInit(&XLogCtl->Insert.insertpos_lck);
	SpinLockInit(&XLogCtl->info_lck);
	SpinLockInit(&XLogCtl->ulsn_lck);
}

/*
 * 此函数必须在系统安装时调用一次。它创建 pg_control
 * 和初始 XLOG 段。
 */
void BootStrapXLOG(void)
{
	CheckPoint	fc_checkPoint;
	char	   *fc_buffer;
	XLogPageHeader fc_page;
	XLogLongPageHeader fc_longpage;
	XLogRecord *fc_record;
	char	   *fc_recptr;
	uint64		fc_sysidentifier;
	struct timeval fc_tv;
	pg_crc32c	fc_crc;

	/* 允许普通的 WAL 段创建，例如 StartupXLOG() 会 */
	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	XLogCtl->InstallXLogFileSegmentActive = true;
	LWLockRelease(ControlFileLock);

	/*
	 * 选择一个尽可能唯一的系统标识码用于此安装。
	 * 我们使用 gettimeofday() 的结果，包括小数秒
	 * 字段，这大概是我们能得到的最唯一的标识。（不要
	 * 使用 random()，因为它没有被初始化，并且没有可移植的方式
	 * 除了系统时钟值来初始化它……）uint64 值的上半部分
	 * 只是 tv_sec 部分，而下半部分包含
	 * tv_usec 部分（必须适合 20 位），加上当前
	 * PID 的 12 位以增加一点额外的唯一性。一个了解这种编码的人
	 * 可以确定安装的初始化时间，这可能
	 * 有时是有用的。
	 */
	gettimeofday(&fc_tv, NULL);
	fc_sysidentifier = ((uint64) fc_tv.tv_sec) << 32;
	fc_sysidentifier |= ((uint64) fc_tv.tv_usec) << 12;
	fc_sysidentifier |= getpid() & 0xFFF;

	/* 页面缓冲区必须适当地对齐以支持 O_DIRECT */
	fc_buffer = (char *) palloc(XLOG_BLCKSZ + XLOG_BLCKSZ);
	fc_page = (XLogPageHeader) TYPEALIGN(XLOG_BLCKSZ, fc_buffer);
	memset(fc_page, 0, XLOG_BLCKSZ);

	/*
	 * 设置初始检查点记录的信息
	 *
	 * 初始检查点记录写入 WAL
	 * 段的开始，logid=0 logseg=1。第一个 WAL 段 0/0
	 * 未被使用，因此我们可以使用 0/0 来表示“在任何有效 WAL 段之前”。
	 */
	fc_checkPoint.redo = wal_segment_size + SizeOfXLogLongPHD;
	fc_checkPoint.ThisTimeLineID = BootstrapTimeLineID;
	fc_checkPoint.PrevTimeLineID = BootstrapTimeLineID;
	fc_checkPoint.fullPageWrites = fullPageWrites;
	fc_checkPoint.nextXid =
		FullTransactionIdFromEpochAndXid(0, FirstNormalTransactionId);
	fc_checkPoint.nextOid = FirstGenbkiObjectId;
	fc_checkPoint.nextMulti = FirstMultiXactId;
	fc_checkPoint.nextMultiOffset = 0;
	fc_checkPoint.oldestXid = FirstNormalTransactionId;
	fc_checkPoint.oldestXidDB = Template1DbOid;
	fc_checkPoint.oldestMulti = FirstMultiXactId;
	fc_checkPoint.oldestMultiDB = Template1DbOid;
	fc_checkPoint.oldestCommitTsXid = InvalidTransactionId;
	fc_checkPoint.newestCommitTsXid = InvalidTransactionId;
	fc_checkPoint.time = (pg_time_t) time(NULL);
	fc_checkPoint.oldestActiveXid = InvalidTransactionId;

	ShmemVariableCache->nextXid = fc_checkPoint.nextXid;
	ShmemVariableCache->nextOid = fc_checkPoint.nextOid;
	ShmemVariableCache->oidCount = 0;
	MultiXactSetNextMXact(fc_checkPoint.nextMulti, fc_checkPoint.nextMultiOffset);
	AdvanceOldestClogXid(fc_checkPoint.oldestXid);
	SetTransactionIdLimit(fc_checkPoint.oldestXid, fc_checkPoint.oldestXidDB);
	SetMultiXactIdLimit(fc_checkPoint.oldestMulti, fc_checkPoint.oldestMultiDB, true);
	SetCommitTsLimit(InvalidTransactionId, InvalidTransactionId);

	/* 设置 XLOG 页面头 */
	fc_page->xlp_magic = XLOG_PAGE_MAGIC;
	fc_page->xlp_info = XLP_LONG_HEADER;
	fc_page->xlp_tli = BootstrapTimeLineID;
	fc_page->xlp_pageaddr = wal_segment_size;
	fc_longpage = (XLogLongPageHeader) fc_page;
	fc_longpage->xlp_sysid = fc_sysidentifier;
	fc_longpage->xlp_seg_size = wal_segment_size;
	fc_longpage->xlp_xlog_blcksz = XLOG_BLCKSZ;

	/* 插入初始检查点记录 */
	fc_recptr = ((char *) fc_page + SizeOfXLogLongPHD);
	fc_record = (XLogRecord *) fc_recptr;
	fc_record->xl_prev = 0;
	fc_record->xl_xid = InvalidTransactionId;
	fc_record->xl_tot_len = SizeOfXLogRecord + SizeOfXLogRecordDataHeaderShort + sizeof(fc_checkPoint);
	fc_record->xl_info = XLOG_CHECKPOINT_SHUTDOWN;
	fc_record->xl_rmid = RM_XLOG_ID;
	fc_recptr += SizeOfXLogRecord;
	/* 填充 XLogRecordDataHeaderShort 结构 */
	*(fc_recptr++) = (char) XLR_BLOCK_ID_DATA_SHORT;
	*(fc_recptr++) = sizeof(fc_checkPoint);
	memcpy(fc_recptr, &fc_checkPoint, sizeof(fc_checkPoint));
	fc_recptr += sizeof(fc_checkPoint);
	Assert(fc_recptr - (char *) fc_record == fc_record->xl_tot_len);

	INIT_CRC32C(fc_crc);
	COMP_CRC32C(fc_crc, ((char *) fc_record) + SizeOfXLogRecord, fc_record->xl_tot_len - SizeOfXLogRecord);
	COMP_CRC32C(fc_crc, (char *) fc_record, offsetof(XLogRecord, xl_crc));
	FIN_CRC32C(fc_crc);
	fc_record->xl_crc = fc_crc;

	/* 创建第一个 XLOG 段文件 */
	openLogTLI = BootstrapTimeLineID;
	openLogFile = XLogFileInit(1, BootstrapTimeLineID);

	/*
	 * 我们这里不必担心 Reserve/ReleaseExternalFD，因为我们将
	 * 立即再次关闭文件。
	 */

	/* 使用初始记录写入第一页 */
	errno = 0;
	pgstat_report_wait_start(WAIT_EVENT_WAL_BOOTSTRAP_WRITE);
	if (write(openLogFile, fc_page, XLOG_BLCKSZ) != XLOG_BLCKSZ)
	{
		/* 如果写入未设置 errno，假设问题是没有磁盘空间 */
		if (errno == 0)
			errno = ENOSPC;
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not write bootstrap write-ahead log file: %m")));
	}
	pgstat_report_wait_end();

	pgstat_report_wait_start(WAIT_EVENT_WAL_BOOTSTRAP_SYNC);
	if (pg_fsync(openLogFile) != 0)
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not fsync bootstrap write-ahead log file: %m")));
	pgstat_report_wait_end();

	if (close(openLogFile) != 0)
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg("could not close bootstrap write-ahead log file: %m")));

	openLogFile = -1;

	/* 现在创建 pg_control */
	fc_InitControlFile(fc_sysidentifier);
	ControlFile->time = fc_checkPoint.time;
	ControlFile->checkPoint = fc_checkPoint.redo;
	ControlFile->checkPointCopy = fc_checkPoint;

#ifdef FDD
	ControlFile->encrypted = FDB_ENCRYPT_TYPE_NONE;	
#endif //FDD
	/* 在 WriteControlFile() 中设置一些额外的 ControlFile 字段 */
	fc_WriteControlFile();

	/* 也引导提交日志 */
	BootStrapCLOG();
	BootStrapCommitTs();
	BootStrapSUBTRANS();
	BootStrapMultiXact();

	pfree(fc_buffer);

	/*
	 * 强制读取控制文件 - 与正常处理相比，我们
	 * 否则不会运行其中的检查和 GUC 相关初始化。
	 */
	fc_ReadControlFile();
}

static char * fc_str_time(pg_time_t fc_tnow)
{
	static char fc_buf[128];

	pg_strftime(fc_buf, sizeof(fc_buf),
				"%Y-%m-%d %H:%M:%S %Z",
				pg_localtime(&fc_tnow, log_timezone));

	return fc_buf;
}

/*
 * 在新时间线初始化第一个 WAL 段。
 */
static void fc_XLogInitNewTimeline(TimeLineID fc_endTLI, XLogRecPtr fc_endOfLog, TimeLineID fc_newTLI)
{
	char		fc_xlogfname[MAXFNAMELEN];
	XLogSegNo	fc_endLogSegNo;
	XLogSegNo	fc_startLogSegNo;

	/* 在归档恢复后我们总是切换到新时间线 */
	Assert(fc_endTLI != fc_newTLI);

	/*
	 * 最后一次更新最小恢复点。
	 */
	fc_UpdateMinRecoveryPoint(InvalidXLogRecPtr, true);

	/*
	 * 计算旧时间线上的最后一个段，以及新时间线上的第一个段。
	 * 如果切换发生在段的中间，它们是相同的，但如果切换
	 * 正好发生在段边界处，startLogSegNo 将是 endLogSegNo + 1。
	 */
	XLByteToPrevSeg(fc_endOfLog, fc_endLogSegNo, wal_segment_size);
	XLByteToSeg(fc_endOfLog, fc_startLogSegNo, wal_segment_size);

	/*
	 * 初始化新时间线的起始 WAL 段。如果切换
	 * 发生在段的中间，将旧时间线的最后一个 WAL 段
	 * 中切换点之前的数据复制到新时间线的起始 WAL 段。
	 */
	if (fc_endLogSegNo == fc_startLogSegNo)
	{
		/*
		 * 在新时间线上复制文件。
		 *
		 * 目前不允许写入 WAL，因此没有锁定
		 * 的考虑。但我们应该和 XLogFileInit 一样紧张，以
		 * 避免放置一个虚假的文件。
		 */
		fc_XLogFileCopy(fc_newTLI, fc_endLogSegNo, fc_endTLI, fc_endLogSegNo,
					 XLogSegmentOffset(fc_endOfLog, wal_segment_size));
	}
	else
	{
		/*
		 * 切换发生在段边界，因此只需在新时间线创建下一个
		 * 段。
		 */
		int			fc_fd;

		fc_fd = XLogFileInit(fc_startLogSegNo, fc_newTLI);

		if (close(fc_fd) != 0)
		{
			char		fc_xlogfname[MAXFNAMELEN];
			int			fc_save_errno = errno;

			XLogFileName(fc_xlogfname, fc_newTLI, fc_startLogSegNo, wal_segment_size);
			errno = fc_save_errno;
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not close file \"%s\": %m", fc_xlogfname)));
		}
	}

	/*
	 * 让我们确认一下新的段落没有 .ready 或 .done 标志被张贴。
	 */
	XLogFileName(fc_xlogfname, fc_newTLI, fc_startLogSegNo, wal_segment_size);
	XLogArchiveCleanup(fc_xlogfname);
}

/*
 * 在归档恢复结束时执行清理操作。
 */
static void fc_CleanupAfterArchiveRecovery(TimeLineID fc_EndOfLogTLI, XLogRecPtr fc_EndOfLog,
							TimeLineID fc_newTLI)
{
	/*
	 * 执行 recovery_end_command（如果有的话）。
	 */
	if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0)
		ExecuteRecoveryCommand(recoveryEndCommand,
							   "recovery_end_command",
							   true,
							   WAIT_EVENT_RECOVERY_END_COMMAND);

	/*
	 * 我们切换到了一个新的时间线。清理旧时间线上的段落。
	 *
	 * 如果旧时间线上的段落编号较高，则将其删除。
	 * 它们可能包含有效的 WAL，但也可能是包含垃圾的预分配文件。
	 * 无论如何，它们都不是新时间线历史的一部分，所以我们不需要它们。
	 */
	RemoveNonParentXlogFiles(fc_EndOfLog, fc_newTLI);

	/*
	 * 如果切换发生在一个段落的中间，旧时间线上的最后一个部分段该如何处理？
	 * 如果我们不归档它，而创建 WAL 的服务器也从未归档它（例如，因为
	 * 它被陨石击中），那么它将永远不会被归档。从我们的角度来看，这没问题，
	 * 因为我们用新 TLI 创建的新段包含了旧时间线在切换点之前所有的 WAL。
	 * 但是如果您稍后尝试执行 PITR 来访问旧时间线上的“丢失” WAL，恢复将无法在
	 * 归档中找到它。它在具有新 TLI 的新文件中实际存在，但在恢复到旧时间线时
	 * 恢复不会看那里。另一方面，如果我们归档了部分段，而该时间线上的原始服务器
	 * 仍在运行并且稍后归档同一段的完整版本，它将失败。（我们在 9.4 及以下版本中
	 * 是这样做的，这造成了这样的问题）。
	 *
	 * 作为折衷，我们将最后一个段重命名为 .partial 后缀，并归档它。
	 * 归档恢复永远不会尝试读取 .partial 段，因此它们通常不会被使用。
	 * 但在奇怪的 PITR 情况中，管理员可以将它们手动复制到 pg_wal 目录
	 * （去掉后缀）。它们在调试中也可能很有用。
	 *
	 * 如果旧时间线已有 .done 或 .ready 文件，我们已确定该段是完整的，
	 * 所以可以让它正常归档。（特别是如果它最初是从归档中恢复的，
	 * 预期会有 .done 文件）。
	 */
	if (XLogSegmentOffset(fc_EndOfLog, wal_segment_size) != 0 &&
		XLogArchivingActive())
	{
		char		fc_origfname[MAXFNAMELEN];
		XLogSegNo	fc_endLogSegNo;

		XLByteToPrevSeg(fc_EndOfLog, fc_endLogSegNo, wal_segment_size);
		XLogFileName(fc_origfname, fc_EndOfLogTLI, fc_endLogSegNo, wal_segment_size);

		if (!XLogArchiveIsReadyOrDone(fc_origfname))
		{
			char		fc_origpath[MAXPGPATH];
			char		fc_partialfname[MAXFNAMELEN];
			char		fc_partialpath[MAXPGPATH];

			XLogFilePath(fc_origpath, fc_EndOfLogTLI, fc_endLogSegNo, wal_segment_size);
			snprintf(fc_partialfname, MAXFNAMELEN, "%s.partial", fc_origfname);
			snprintf(fc_partialpath, MAXPGPATH, "%s.partial", fc_origpath);

			/*
			 * 确保 .partial 文件没有 .done 或 .ready 文件。
			 */
			XLogArchiveCleanup(fc_partialfname);

			durable_rename(fc_origpath, fc_partialpath, ERROR);
			XLogArchiveNotify(fc_partialfname);
		}
	}
}

/*
 * 检查此服务器上的所需参数是否对于恢复操作的各个方面设置得足够高。
 *
 * 请注意，此函数测试的所有参数需要在 high-availability.sgml 的管理员概述部分中列出。
 * 如果您更改它们，请不要忘记更新列表。
 */
static void fc_CheckRequiredParameterValues(void)
{
	/*
	 * 对于归档恢复，WAL 必须以至少 'replica' wal_level 生成。
	 */
	if (ArchiveRecoveryRequested && ControlFile->wal_level == WAL_LEVEL_MINIMAL)
	{
		ereport(FATAL,
				(errmsg("WAL was generated with wal_level=minimal, cannot continue recovering"),
				 errdetail("This happens if you temporarily set wal_level=minimal on the server."),
				 errhint("Use a backup taken after setting wal_level to higher than minimal.")));
	}

	/*
	 * 对于热备用，WAL 必须以 'replica' 模式生成，并且我们必须至少拥有与主服务器
	 * 相同数量的后端槽位。
	 */
	if (ArchiveRecoveryRequested && EnableHotStandby)
	{
		/* 在进行此测试时，我们忽略 autovacuum_max_workers。 */
		RecoveryRequiresIntParameter("max_connections",
									 MaxConnections,
									 ControlFile->MaxConnections);
		RecoveryRequiresIntParameter("max_worker_processes",
									 max_worker_processes,
									 ControlFile->max_worker_processes);
		RecoveryRequiresIntParameter("max_wal_senders",
									 max_wal_senders,
									 ControlFile->max_wal_senders);
		RecoveryRequiresIntParameter("max_prepared_transactions",
									 max_prepared_xacts,
									 ControlFile->max_prepared_xacts);
		RecoveryRequiresIntParameter("max_locks_per_transaction",
									 max_locks_per_xact,
									 ControlFile->max_locks_per_xact);
	}
}

/*
 * 这必须在主进程或独立后台启动时调用一次
 */
void StartupXLOG(void)
{
	XLogCtlInsert *fc_Insert;
	CheckPoint	fc_checkPoint;
	bool		fc_wasShutdown;
	bool		fc_didCrash;
	bool		fc_haveTblspcMap;
	bool		fc_haveBackupLabel;
	XLogRecPtr	fc_EndOfLog;
	TimeLineID	fc_EndOfLogTLI;
	TimeLineID	fc_newTLI;
	bool		fc_performedWalRecovery;
	EndOfWalRecoveryInfo *fc_endOfRecoveryInfo;
	XLogRecPtr	fc_abortedRecPtr;
	XLogRecPtr	fc_missingContrecPtr;
	TransactionId fc_oldestActiveXID;
	bool		fc_promoted = false;

	/*
	 * 我们应该有一个辅助进程资源所有者可以使用，并且我们不应该
	 * 在某个其他资源所有者已安装的事务中。
	 */
	Assert(AuxProcessResourceOwner != NULL);
	Assert(CurrentResourceOwner == NULL ||
		   CurrentResourceOwner == AuxProcessResourceOwner);
	CurrentResourceOwner = AuxProcessResourceOwner;

	/*
	 * 检查内容是否有效。
	 */
	if (!XRecOffIsValid(ControlFile->checkPoint))
		ereport(FATAL,
				(errmsg("control file contains invalid checkpoint location")));

	switch (ControlFile->state)
	{
		case DB_SHUTDOWNED:

			/*
			 * 这是预期的情况，因此在独立模式下不要过于啰嗦。
			 */
			ereport(IsPostmasterEnvironment ? LOG : NOTICE,
					(errmsg("database system was shut down at %s",
							fc_str_time(ControlFile->time))));
			break;

		case DB_SHUTDOWNED_IN_RECOVERY:
			ereport(LOG,
					(errmsg("database system was shut down in recovery at %s",
							fc_str_time(ControlFile->time))));
			break;

		case DB_SHUTDOWNING:
			ereport(LOG,
					(errmsg("database system shutdown was interrupted; last known up at %s",
							fc_str_time(ControlFile->time))));
			break;

		case DB_IN_CRASH_RECOVERY:
			ereport(LOG,
					(errmsg("database system was interrupted while in recovery at %s",
							fc_str_time(ControlFile->time)),
					 errhint("This probably means that some data is corrupted and"
							 " you will have to use the last backup for recovery.")));
			break;

		case DB_IN_ARCHIVE_RECOVERY:
			ereport(LOG,
					(errmsg("database system was interrupted while in recovery at log time %s",
							fc_str_time(ControlFile->checkPointCopy.time)),
					 errhint("If this has occurred more than once some data might be corrupted"
							 " and you might need to choose an earlier recovery target.")));
			break;

		case DB_IN_PRODUCTION:
			ereport(LOG,
					(errmsg("database system was interrupted; last known up at %s",
							fc_str_time(ControlFile->time))));
			break;

		default:
			ereport(FATAL,
					(errmsg("control file contains invalid database cluster state")));
	}

	/* 这只是为了允许使用调试器附加到启动进程 */
#ifdef XLOG_REPLAY_DELAY
	if (ControlFile->state != DB_SHUTDOWNED)
		pg_usleep(60000000L);
#endif

	/*
	 * 验证 pg_wal 和 pg_wal/archive_status 是否存在。 在某些
	 * 进行 PITR 复制的情况下，这些目录可能已被排除，需要重新创建。
	 */
	fc_ValidateXLOGDirectoryStructure();

	/* 设置报告启动进度所需的超时处理程序。 */
	if (!IsBootstrapProcessingMode())
		RegisterTimeout(STARTUP_PROGRESS_TIMEOUT,
						startup_progress_timeout_handler);

	/*----------
	 * 如果我们之前崩溃，执行几个操作：
	 *
	 * - pg_wal 目录中可能仍包含一些用于创建新段的临时 WAL
	 *   段，因此需要进行一些清理，以免使此路径膨胀。 首先执行
	 *   这一步，因为同步这些临时数据没有意义。
	 *
	 * - 可能有我们已写入、打算进行 fsync 的数据，但
	 *   实际上尚未 fsync。因此，近期的电源故障可能导致早期未刷新的
	 *   写入丢失，即使从现在开始写入磁盘的更新数据将被保留。
	 *   为了避免这种情况，fsync 整个数据目录。
	 */
	if (ControlFile->state != DB_SHUTDOWNED &&
		ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
	{
		fc_RemoveTempXlogFiles();
		SyncDataDirectory();
		fc_didCrash = true;
	}
	else
		fc_didCrash = false;

	/*
	 * 如果需要，为 WAL 恢复做好准备。
	 *
	 * InitWalRecovery 分析控制文件和备份标签文件（如果有）。 
	 * 它根据起始检查点更新内存中的 ControlFile 缓冲区，并设置 
	 * InRecovery 和 ArchiveRecoveryRequested。 它还应用表空间
	 * 映射文件（如果有）。
	 */
	InitWalRecovery(ControlFile, &fc_wasShutdown,
					&fc_haveBackupLabel, &fc_haveTblspcMap);
	fc_checkPoint = ControlFile->checkPointCopy;

	/* 从检查点记录初始化共享内存变量 */
	ShmemVariableCache->nextXid = fc_checkPoint.nextXid;
	ShmemVariableCache->nextOid = fc_checkPoint.nextOid;
	ShmemVariableCache->oidCount = 0;
	MultiXactSetNextMXact(fc_checkPoint.nextMulti, fc_checkPoint.nextMultiOffset);
	AdvanceOldestClogXid(fc_checkPoint.oldestXid);
	SetTransactionIdLimit(fc_checkPoint.oldestXid, fc_checkPoint.oldestXidDB);
	SetMultiXactIdLimit(fc_checkPoint.oldestMulti, fc_checkPoint.oldestMultiDB, true);
	SetCommitTsLimit(fc_checkPoint.oldestCommitTsXid,
					 fc_checkPoint.newestCommitTsXid);
	XLogCtl->ckptFullXid = fc_checkPoint.nextXid;

	/*
	 * 清除任何旧的 relcache 缓存文件。 如果我们进行任何 WAL 
	 * 回放，这 *是必要的*，因为这可能会导致缓存文件与数据库现实不同步。
	 * 理论上，如果数据库已经干净地关闭，我们可以留下它们，但将它们
	 * 始终删除并在第一次后台启动期间重建它们似乎是最安全的。 这些
	 * 文件需要从包括 pg_tblspc 在内的所有目录中删除，但是符号链接
	 * 仅在从备份进行归档恢复时读取 tablespace_map 文件后创建，
	 * 因此在创建符号链接后这里需要清除旧的 relcache 文件。
	 */
	RelationCacheInitFileRemove();

	/*
	 * 在有机会删除所需资源之前初始化复制槽。
	 */
#ifdef FDD
	/*	 
	 * 如果我们在归档恢复中，则非故障转移槽将不再有用，应在启动
	 * 期间被删除。
	 */
	StartupReplicationSlots(ArchiveRecoveryRequested && StandbyMode);
#else
	
	StartupReplicationSlots();
#endif


	/*
	 * 启动逻辑状态，需要现在设置，以便在崩溃恢复期间有
	 * 合适的数据。
	 */
	StartupReorderBuffer();

	/*
	 * 启动 CLOG。这必须在 ShmemVariableCache->nextXid 初始化后，且在我们接受连接或开始 WAL 回放之前完成。
	 */
	StartupCLOG();

	/*
	 * 启动 MultiXact。我们需要尽早完成此操作，以便能够回放截断。
	 */
	StartupMultiXact();

	/*
	 * 提交时间戳同样如此。如果控制文件中的设置启用，则激活该功能，因为在设置禁用时不应跟踪提交时间戳。当回放 XLOG_PARAMETER_CHANGE 记录时，这个功能可以被启动或停止。
	 */
	if (ControlFile->track_commit_timestamp)
		StartupCommitTs();

	/*
	 * 恢复关于已知复制伙伴的回放进度的知识。
	 */
	StartupReplicationOrigin();

	/*
	 * 初始化未记录的 LSN。在干净关闭时，它是从控制文件中恢复的。在恢复时，所有未记录的关系都会被清除，因此未记录的 LSN 计数器也可以重置。
	 */
	if (ControlFile->state == DB_SHUTDOWNED)
		XLogCtl->unloggedLSN = ControlFile->unloggedLSN;
	else
		XLogCtl->unloggedLSN = FirstNormalUnloggedLSN;

	/*
	 * 在“现在”和恢复目标时间线之间从归档复制任何缺失的时间线历史文件到 pg_wal。虽然我们自己不需要那些文件——恢复目标时间线的历史文件也覆盖了历史中所有先前的时间线——但级联备用服务器可能对此感兴趣。或者，如果你将 WAL 从此服务器归档到与主服务器不同的归档，这样在故障转移后将所有历史文件归档到那里，以便你可以使用其中一个旧的时间线作为 PITR 目标，这样做是好的。时间线历史文件很小，因此复制它们比不复制以及后来感到遗憾要好。
	 */
	restoreTimeLineHistoryFiles(fc_checkPoint.ThisTimeLineID, recoveryTargetTLI);

	/*
	 * 在执行恢复之前，扫描 pg_twophase 并填充其状态，以便能够处理由重做生成的条目。在采取任何恢复操作之前进行扫描的好处在于，可以丢弃比要回放的第一条记录更新的任何 2PC 文件，从而避免回放时发生冲突。这也避免了在恢复磁盘上的两阶段数据时后续的任何扫描。
	 */
	restoreTwoPhaseData();

	/*
	 * 在进行崩溃恢复时，重置 pgstat 数据——它可能无效。否则，恢复 pgstat 数据。在这里这样做是安全的，因为后端进程尚未启动。
	 *
	 * 注意：恢复复制槽统计依赖于槽状态已从磁盘恢复。
	 *
	 * TODO：通过额外的工作，我们可以直接从我们要从中启动的检查点重做位置关联的 pgstat 文件开始。
	 */
	if (fc_didCrash)
		pgstat_discard_stats();
	else
		pgstat_restore_stats();

	lastFullPageWrites = fc_checkPoint.fullPageWrites;

	RedoRecPtr = XLogCtl->RedoRecPtr = XLogCtl->Insert.RedoRecPtr = fc_checkPoint.redo;
	doPageWrites = lastFullPageWrites;

	/* REDO */
	if (InRecovery)
	{
		/* 初始化 RecoveryInProgress() 的状态 */
		SpinLockAcquire(&XLogCtl->info_lck);
		if (InArchiveRecovery)
			XLogCtl->SharedRecoveryState = RECOVERY_STATE_ARCHIVE;
		else
			XLogCtl->SharedRecoveryState = RECOVERY_STATE_CRASH;
		SpinLockRelease(&XLogCtl->info_lck);

		/*
		 * 更新 pg_control 以显示我们正在恢复，并显示选定的检查点作为我们开始的位置。我们还通过从备份历史文件中获得的任何最小恢复停止点来标记 pg_control。
		 *
		 * 此时无需持有 ControlFileLock，因为我们还没有足够的状态。
		 */
		fc_UpdateControlFile();

		/*
		 * 如果存在备份标签文件，它的工作已经完成，信息
		 * 现在已经传播到pg_control中。我们必须删除
		 * 标签文件，以便在恢复期间崩溃时，我们将从
		 * 最新的恢复重启点开始，而不是全部恢复到
		 * 备份起始点。不过，将文件重命名避开
		 * 而不是完全删除似乎更为谨慎。
		 */
		if (fc_haveBackupLabel)
		{
			unlink(BACKUP_LABEL_OLD);
			durable_rename(BACKUP_LABEL_FILE, BACKUP_LABEL_OLD, FATAL);
		}

		/*
		 * 如果存在tablespace_map文件，它的工作已经完成，
		 * symlinks已经创建。我们必须删除映射文件，以便在
		 * 恢复期间崩溃时，不再创建symlinks。
		 * 不过，将文件重命名避开而不是完全删除似乎
		 * 更为谨慎。
		 */
		if (fc_haveTblspcMap)
		{
			unlink(TABLESPACE_MAP_OLD);
			durable_rename(TABLESPACE_MAP, TABLESPACE_MAP_OLD, FATAL);
		}

		/*
		 * 初始化minRecoveryPoint的本地副本。当进行崩溃
		 * 恢复时，我们希望重放直到WAL的末尾。特别是，在
		 * 提升的备用中，控制文件中的minRecoveryPoint值
		 * 仅在第一次检查点之后更新。然而，
		 * 如果实例在第一次后恢复检查点完成之前崩溃，
		 * 那么恢复将使用一个过时的位置，导致启动过程
		 * 认为在检查数据一致性时仍然存在无效的页面
		 * 引用。
		 */
		if (InArchiveRecovery)
		{
			LocalMinRecoveryPoint = ControlFile->minRecoveryPoint;
			LocalMinRecoveryPointTLI = ControlFile->minRecoveryPointTLI;
		}
		else
		{
			LocalMinRecoveryPoint = InvalidXLogRecPtr;
			LocalMinRecoveryPointTLI = 0;
		}

		/* 检查用于生成WAL的GUC允许恢复 */
		fc_CheckRequiredParameterValues();

		/*
		 * 我们正在进行恢复，因此未记录的关系可能会被损坏，
		 * 必须重置。这应该在允许热备用
		 * 连接之前完成，以便只读后端不会尝试读取之前
		 * 残留的垃圾。
		 */
		ResetUnloggedRelations(UNLOGGED_RELATION_CLEANUP);

		/*
		 * 同样，删除由崩溃的后端留下的任何保存的事务快照文件。
		 */
		DeleteAllExportedSnapshotFiles();

		/*
		 * 如果启用，则初始化热备用。我们不会让后端
		 * 进入，直到我们达到控制文件中指定的最小恢复点，
		 * 并且我们已经从运行中的xacts WAL记录建立了恢复快照。
		 */
		if (ArchiveRecoveryRequested && EnableHotStandby)
		{
			TransactionId *fc_xids;
			int			fc_nxids;

			ereport(DEBUG1,
					(errmsg_internal("initializing for hot standby")));

			InitRecoveryTransactionEnvironment();

			if (fc_wasShutdown)
				fc_oldestActiveXID = PrescanPreparedTransactions(&fc_xids, &fc_nxids);
			else
				fc_oldestActiveXID = fc_checkPoint.oldestActiveXid;
			Assert(TransactionIdIsValid(fc_oldestActiveXID));

			/* 告诉procarray它必须处理的xids范围 */
			ProcArrayInitRecovery(XidFromFullTransactionId(ShmemVariableCache->nextXid));

			/*
			 * 仅启动subtrans。CLOG、MultiXact和提交时间戳
			 * 已经启动，其他SLRU在恢复期间不再维护，
			 * 现在无需启动。
			 */
			StartupSUBTRANS(fc_oldestActiveXID);

			/*
			 * 如果我们从一个关闭检查点开始，我们知道在这一点上
			 * 主服务器上没有任何运行。所以伪装一个
			 * 空的运行中的xacts记录，并在这里和现在使用它。
			 * 恢复准备事务的附加备用状态。
			 */
			if (fc_wasShutdown)
			{
				RunningTransactionsData fc_running;
				TransactionId fc_latestCompletedXid;

				/* 更新任何准备事务的pg_subtrans条目 */
				StandbyRecoverPreparedTransactions();

				/*
				 * 构建一个表示关闭服务器的RunningTransactions快照，
				 * 仅有仍然存活的准备事务。在这一点上我们永远不会溢出，
				 * 因为所有子xids都与其父准备事务一起列出。
				 */
				fc_running.xcnt = fc_nxids;
				fc_running.subxcnt = 0;
				fc_running.subxid_status = SUBXIDS_IN_SUBTRANS;
				fc_running.nextXid = XidFromFullTransactionId(fc_checkPoint.nextXid);
				fc_running.oldestRunningXid = fc_oldestActiveXID;
				fc_latestCompletedXid = XidFromFullTransactionId(fc_checkPoint.nextXid);
				TransactionIdRetreat(fc_latestCompletedXid);
				Assert(TransactionIdIsNormal(fc_latestCompletedXid));
				fc_running.latestCompletedXid = fc_latestCompletedXid;
				fc_running.xids = fc_xids;

				ProcArrayApplyRecoveryInfo(&fc_running);
			}
		}

		/*
		 * 现在我们已经准备好重放WAL。开始吧。
		 */
		PerformWalRecovery();
		fc_performedWalRecovery = true;
	}
	else
		fc_performedWalRecovery = false;

	/*
	 * 完成WAL恢复。
	 */
	fc_endOfRecoveryInfo = FinishWalRecovery();
	fc_EndOfLog = fc_endOfRecoveryInfo->endOfLog;
	fc_EndOfLogTLI = fc_endOfRecoveryInfo->endOfLogTLI;
	fc_abortedRecPtr = fc_endOfRecoveryInfo->abortedRecPtr;
	fc_missingContrecPtr = fc_endOfRecoveryInfo->missingContrecPtr;

	/*
	 * 重置 ps 状态显示，以便没有与恢复相关的信息显示。
	 */
	set_ps_display("");

	/*
	 * 当从备份中恢复时（我们处于恢复状态，并且请求了归档恢复），
	 * 如果我们没有向前推进到数据库一致的点，则进行抱怨。
	 * 对于常规的在线从主数据库备份，这意味着达到备份结束的 WAL 
	 * 记录（此时我们将 backupStartPoint 重置为无效），对于从副本
	 * 备份（无法将记录注入 WAL 流），该点是当我们到达 pg_control
	 * 中的 minRecoveryPoint（在从副本备份时我们故意将其最后一
	 * 次复制）的时刻。对于 pg_rewind（创建了带有 "pg_rewind"
	 * 方法的 backup_label）或快照式备份（没有），backupEndRequired
	 * 将被设置为 false。
	 *
	 * 注意：此处查看局部变量 LocalMinRecoveryPoint 实际上是可以的，
	 * 尽管 ControlFile->minRecoveryPoint 可能更为领先——
	 * ControlFile->minRecoveryPoint 不能超过我们处理的 WAL。
	 */
	if (InRecovery &&
		(fc_EndOfLog < LocalMinRecoveryPoint ||
		 !XLogRecPtrIsInvalid(ControlFile->backupStartPoint)))
	{
		/*
		 * 在到达备份结束的 WAL 记录或 minRecoveryPoint 之前
		 * 超出 WAL 的末尾。这是一个不好的迹象，表明您尝试从在线
		 * 备份恢复但从未调用 pg_backup_stop()，或您没有归档
		 * 所需的所有 WAL。
		 */
		if (ArchiveRecoveryRequested || ControlFile->backupEndRequired)
		{
			if (!XLogRecPtrIsInvalid(ControlFile->backupStartPoint) || ControlFile->backupEndRequired)
				ereport(FATAL,
						(errmsg("WAL ends before end of online backup"),
						 errhint("All WAL generated while online backup was taken must be available at recovery.")));
			else
				ereport(FATAL,
						(errmsg("WAL ends before consistent recovery point")));
		}
	}

	/*
	 * 将未记录的关系重置为其 INIT 分支的内容。此操作在恢复
	 * 完成之后进行，以包含在恢复期间创建的任何未记录关系，但
	 * 在将恢复标记为成功完成之前进行。否则如果任何在恢复结束
	 * 后的步骤失败，我们将不会重试。
	 */
	if (InRecovery)
		ResetUnloggedRelations(UNLOGGED_RELATION_INIT);

	/*
	 * 预扫描已准备的事务，以找出存在的 XID 范围。此信息尚不
	 * 完全需要，但将其放在这里是为了能够在进行任何磁盘更改
	 * 之前检测潜在问题。
	 */
	fc_oldestActiveXID = PrescanPreparedTransactions(NULL, NULL);

	/*
	 * 在可能切换到新时间线之前，允许创建普通的 WAL 段，并且
	 * 在最后一次 ReadRecord() 之后。
	 */
	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	XLogCtl->InstallXLogFileSegmentActive = true;
	LWLockRelease(ControlFileLock);

	/*
	 * 考虑我们是否需要分配一个新的时间线 ID。
	 *
	 * 如果我们进行了归档恢复，我们总是分配一个新的 ID。这处理
	 * 了一些问题。如果我们在恢复过程中没有达到 WAL 的末尾，
	 * 那么我们显然是在生成一个新的时间线，并且必须为其分配
	 * 一个唯一的新 ID。即使我们跑到了最后，修改当前最后的
	 * 段也是有问题的，因为这可能导致尝试覆盖已经归档的该
	 * 段副本，我们鼓励 DBA 使其 archive_commands 拒绝这一点。
	 * 我们可以通过让新的活动段具有新的时间线 ID 来避免
	 * 此问题。
	 *
	 * 在正常的崩溃恢复中，我们可以简单地扩展我们所在的时间线。
	 */
	fc_newTLI = fc_endOfRecoveryInfo->lastRecTLI;
	if (ArchiveRecoveryRequested)
	{
		fc_newTLI = findNewestTimeLine(recoveryTargetTLI) + 1;
		ereport(LOG,
				(errmsg("selected new timeline ID: %u", fc_newTLI)));

		/*
		 * 创建最后一个WAL段的可写副本。 （注意，我们还有旧WAL的最后一个区块的副本在
		 * endOfRecovery->lastPage中；我们将在下面使用。）
		 */
		fc_XLogInitNewTimeline(fc_EndOfLogTLI, fc_EndOfLog, fc_newTLI);

		/*
		 * 移除信号文件，以免我们在后续崩溃中意外重新进入存档恢复模式。
		 */
		if (fc_endOfRecoveryInfo->standby_signal_file_found)
			durable_unlink(STANDBY_SIGNAL_FILE, FATAL);

		if (fc_endOfRecoveryInfo->recovery_signal_file_found)
			durable_unlink(RECOVERY_SIGNAL_FILE, FATAL);

		/*
		 * 写入时间线历史文件，并进行存档。在这一点之后（或更确切地说，一旦文件被存档），时间线
		 * 将在WAL存档和任何备用服务器中显示为“已被采用”。如果我们在实际切换到新时间线之前崩溃，
		 * 备用服务器仍然会认为我们已经切换到新时间线，并会尝试连接到新时间线。
		 * 为了最小化这种情况的窗口，尽量在这里和写入恢复结束记录之间做尽可能少的事情。
		 */
		writeTimeLineHistory(fc_newTLI, recoveryTargetTLI,
							 fc_EndOfLog, fc_endOfRecoveryInfo->recoveryStopReason);

		ereport(LOG,
				(errmsg("archive recovery complete")));
	}

	/* 还要在共享内存中保存选定的TimeLineID */
	XLogCtl->InsertTimeLineID = fc_newTLI;
	XLogCtl->PrevTimeLineID = fc_endOfRecoveryInfo->lastRecTLI;

	/*
	 * 实际上，如果WAL在一个不完整记录中结束，则跳过已经通过的部分，
	 * 从持久化部分之后开始写入。 （首先写入一个OVERWRITE_CONTRECORD消息是至关重要的，
	 * 我们会在开放写入新WAL时立即这样做。）
	 */
	if (!XLogRecPtrIsInvalid(fc_missingContrecPtr))
	{
		/*
		 * 只有在我们不切换到新时间线时，我们才应该有一个missingContrecPtr。
		 * 当发生时间线切换时，WAL从旧时间线复制到新时间线仅到达最后一个完整
		 * 记录的结束，因此不存在需要忽略的不完整WAL记录。
		 */
		Assert(fc_newTLI == fc_endOfRecoveryInfo->lastRecTLI);
		Assert(!XLogRecPtrIsInvalid(fc_abortedRecPtr));
		fc_EndOfLog = fc_missingContrecPtr;
	}

	/*
	 * 准备从EndOfLog位置开始写入WAL，并使用包含
	 * 前一个实例最后一条记录的块初始化xlog缓冲区缓存。
	 */
	fc_Insert = &XLogCtl->Insert;
	fc_Insert->PrevBytePos = fc_XLogRecPtrToBytePos(fc_endOfRecoveryInfo->lastRec);
	fc_Insert->CurrBytePos = fc_XLogRecPtrToBytePos(fc_EndOfLog);

	/*
	 * 这里的关键点是：lastPage包含LastRec
	 * 记录所跨越的*最后*区块，而不是它开始的区块。最后一个区块确实是
	 * 我们想要使用的区块。
	 */
	if (fc_EndOfLog % XLOG_BLCKSZ != 0)
	{
		char	   *fc_page;
		int			fc_len;
		int			fc_firstIdx;

		fc_firstIdx = XLogRecPtrToBufIdx(fc_EndOfLog);
		fc_len = fc_EndOfLog - fc_endOfRecoveryInfo->lastPageBeginPtr;
		Assert(fc_len < XLOG_BLCKSZ);

		/* 复制最后一个区块的有效部分，并将其余部分清零 */
		fc_page = &XLogCtl->pages[fc_firstIdx * XLOG_BLCKSZ];
		memcpy(fc_page, fc_endOfRecoveryInfo->lastPage, fc_len);
		memset(fc_page + fc_len, 0, XLOG_BLCKSZ - fc_len);

		XLogCtl->xlblocks[fc_firstIdx] = fc_endOfRecoveryInfo->lastPageBeginPtr + XLOG_BLCKSZ;
		XLogCtl->InitializedUpTo = fc_endOfRecoveryInfo->lastPageBeginPtr + XLOG_BLCKSZ;
	}
	else
	{
		/*
		 * 没有要复制的部分区块。只需设置InitializedUpTo，并且
		 * 让第一次插入日志记录的尝试来初始化下一个缓冲区。
		 */
		XLogCtl->InitializedUpTo = fc_EndOfLog;
	}

	LogwrtResult.Write = LogwrtResult.Flush = fc_EndOfLog;

	XLogCtl->LogwrtResult = LogwrtResult;

	XLogCtl->LogwrtRqst.Write = fc_EndOfLog;
	XLogCtl->LogwrtRqst.Flush = fc_EndOfLog;

	/*
	 * 在READ WRITE事务开始之前使所有sinval管理的缓存失效。
	 * xl_heap_inplace WAL记录不存储足够的数据
	 * 用于使失效失效。 提交记录（如果有）包含失效信息。
	 * 然而，无论我们是否达到提交记录，就地更新是永久性的。
	 * 幸运的是，只读事务能够容忍缓存未反映最新的就地更新。
	 * 只读事务体验到显著的就地更新如下：
	 *
	 * - relhasindex=true 仅在CREATE INDEX
	 * 事务提交使索引完全可用后影响读取者。
	 *
	 * - datconnlimit=DATCONNLIMIT_INVALID_DB 仅在
	 * InitPostgres() 时影响读取者，并且该读取不使用缓存。
	 *
	 * - relfrozenxid、datfrozenxid、relminmxid和datminmxid对
	 * 读取者没有影响。
	 *
	 * 因此，热备用查询（全部只读）在没有缺失失效的情况下
	 * 正常运行。这避免了在后支中更改WAL格式。
	 */
	SIResetAll();

	/*
	 * 如果需要，预分配额外的日志文件。
	 */
	fc_PreallocXlogFiles(fc_EndOfLog, fc_newTLI);

	/*
	 * 好的，我们正式启动了。
	 */
	InRecovery = false;

	/* 开始archive_timeout计时器和LSN运行 */
	XLogCtl->lastSegSwitchTime = (pg_time_t) time(NULL);
	XLogCtl->lastSegSwitchLSN = fc_EndOfLog;

	/* 同时初始化latestCompletedXid，为nextXid - 1 */
	LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE);
	ShmemVariableCache->latestCompletedXid = ShmemVariableCache->nextXid;
	FullTransactionIdRetreat(&ShmemVariableCache->latestCompletedXid);
	LWLockRelease(ProcArrayLock);

	/*
	 * 启动子事务，如果尚未为热备用完成。 （如果需要，提交
	 * 时间戳将在下面启动。）
	 */
	if (standbyState == STANDBY_DISABLED)
		StartupSUBTRANS(fc_oldestActiveXID);

	/*
	 * 进行任何需要SLRUs的恢复结束操作。
	 */
	TrimCLOG();
	TrimMultiXact();

	/*
	 * 重新加载准备交易的共享内存状态。这需要在重命名最后
	 * 一部分旧时间线之前进行，因为我们可能需要从中恢复一些交易。
	 */
	RecoverPreparedTransactions();

	/* 关闭xlogreader */
	ShutdownWalRecovery();

	/* 仅为该后端启用WAL写入。 */
	fc_LocalSetXLogInsertAllowed();

	/* 如果需要，在做其他事情之前写入overwrite-contrecord */
	if (!XLogRecPtrIsInvalid(fc_abortedRecPtr))
	{
		Assert(!XLogRecPtrIsInvalid(fc_missingContrecPtr));
		fc_CreateOverwriteContrecordRecord(fc_abortedRecPtr, fc_missingContrecPtr, fc_newTLI);
	}

	/*
	 * 在共享内存中更新full_page_writes并在资源管理器写入
	 * 清理WAL记录或写入检查点记录之前写入XLOG_FPW_CHANGE记录。
	 */
	fc_Insert->fullPageWrites = lastFullPageWrites;
	UpdateFullPageWrites();

	/*
	 * 如果需要，在XLOG中发出检查点或恢复结束记录。
	 */
	if (fc_performedWalRecovery)
		fc_promoted = fc_PerformRecoveryXLogAction();

	/*
	 * 如果任何关键GUC发生了变化，在允许
	 * 后端写入WAL之前记录它们。
	 */
	fc_XLogReportParameters();

	/* 如果这是归档恢复，执行恢复后的清理操作。 */
	if (ArchiveRecoveryRequested)
		fc_CleanupAfterArchiveRecovery(fc_EndOfLogTLI, fc_EndOfLog, fc_newTLI);

	/*
	 * 本地WAL插入已启用，因此是时候完成
	 * 提交时间戳的初始化。
	 */
	CompleteCommitTsInitialization();

	/*
	 * 完成恢复结束后的所有操作。
	 *
	 * 现在允许后端写入WAL，并相应地更新控制文件状态。控制后端是否可以写入WAL的SharedRecoveryState在持有ControlFileLock时更新，以防止其他后端查看共享内存中控制文件的不一致状态。在此期间，后端仍然可以写入WAL，而控制文件仍然引用一个不在DB_IN_PRODUCTION状态的系统，同时查看磁盘上的控制文件。
	 *
	 * 此外，我们使用info_lck来更新SharedRecoveryState，以确保在共享内存中没有关于其他最近更新可见性的问题。
	 */
	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	ControlFile->state = DB_IN_PRODUCTION;

	SpinLockAcquire(&XLogCtl->info_lck);
	XLogCtl->SharedRecoveryState = RECOVERY_STATE_DONE;
	SpinLockRelease(&XLogCtl->info_lck);

	fc_UpdateControlFile();
	LWLockRelease(ControlFileLock);

	/*
	 * 关闭恢复环境。这必须发生在RecoverPreparedTransactions()（请参阅lock_twophase_recover()中的注释）之后，并在将SharedRecoveryState切换到RECOVERY_STATE_DONE之后，以免任何构建快照的会话依赖于KnownAssignedXids，因为在此阶段RecoveryInProgress()将返回false。这对于准备好的2PC事务至关重要，因为在恢复结束后它们仍然需要包含在快照中。
	 */
	if (standbyState != STANDBY_DISABLED)
		ShutdownRecoveryTransactionEnvironment();

	/*
	 * 如果有级联的备用服务器连接到我们，提醒任何WAL发送进程注意我们已经被提升。
	 */
	WalSndWakeup();

	/*
	 * 如果这是一次提升，现在请求进行（在线）检查点。这并不是出于一致性的要求，但最后的restartpoint可能比较早，并且在发生崩溃时，从中恢复可能比我们现在不再处于备用模式时适当的时间更长。
	 */
	if (fc_promoted)
		RequestCheckpoint(CHECKPOINT_FORCE);
}

/*
 * 来自PerformWalRecovery()的回调，当我们从崩溃恢复切换到归档恢复模式时调用。相应地更新控制文件。
 */
void SwitchIntoArchiveRecovery(XLogRecPtr fc_EndRecPtr, TimeLineID fc_replayTLI)
{
	/* 将minRecoveryPoint初始化为此记录 */
	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	ControlFile->state = DB_IN_ARCHIVE_RECOVERY;
	if (ControlFile->minRecoveryPoint < fc_EndRecPtr)
	{
		ControlFile->minRecoveryPoint = fc_EndRecPtr;
		ControlFile->minRecoveryPointTLI = fc_replayTLI;
	}
	/* 更新本地副本 */
	LocalMinRecoveryPoint = ControlFile->minRecoveryPoint;
	LocalMinRecoveryPointTLI = ControlFile->minRecoveryPointTLI;

	/*
	 * 启动进程可以从这一点更新其minRecoveryPoint的本地副本。
	 */
	updateMinRecoveryPoint = true;

	fc_UpdateControlFile();

	/*
	 * 我们在持有ControlFileLock上的锁时更新SharedRecoveryState，
	 * 以确保共享内存中的两种状态是一致的。
	 */
	SpinLockAcquire(&XLogCtl->info_lck);
	XLogCtl->SharedRecoveryState = RECOVERY_STATE_ARCHIVE;
	SpinLockRelease(&XLogCtl->info_lck);

	LWLockRelease(ControlFileLock);
}

/*
 * 来自PerformWalRecovery()的回调，当我们达到备份结束时调用。相应地更新控制文件。
 */
void ReachedEndOfBackup(XLogRecPtr fc_EndRecPtr, TimeLineID fc_tli)
{
	/*
	 * 我们已到达基础备份的结束，正如pg_control所示。磁盘上的数据现在是一致的（除非minRecovery点在前面，这可能发生在我们在之前的恢复期间崩溃）。重置backupStartPoint和backupEndPoint，并更新minRecoveryPoint，以确保即使恢复在此后不久停止并重新启动，也不允许在更早的点启动。
	 */
	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);

	if (ControlFile->minRecoveryPoint < fc_EndRecPtr)
	{
		ControlFile->minRecoveryPoint = fc_EndRecPtr;
		ControlFile->minRecoveryPointTLI = fc_tli;
	}

	ControlFile->backupStartPoint = InvalidXLogRecPtr;
	ControlFile->backupEndPoint = InvalidXLogRecPtr;
	ControlFile->backupEndRequired = false;
	fc_UpdateControlFile();

	LWLockRelease(ControlFileLock);
}

/*
 * 在 REDO 结束时执行必要的 XLOG 操作。
 *
 * 目标是确保如果我们再次崩溃，我们能够正确恢复。如果我们选择写入检查点，将写入一个关闭检查点，而不是在线检查点。这并不是特别关键，但由于我们可能会分配一个新的 TLI，使用关闭检查点允许我们遵循 TLI 仅在关闭检查点中更改的规则，这在 xlog_redo 中允许一些额外的错误检查。
 */
static bool fc_PerformRecoveryXLogAction(void)
{
	bool		fc_promoted = false;

	/*
	 * 执行检查点以将我们所有的恢复活动更新到磁盘。
	 *
	 * 注意，我们写入的是关闭检查点，而不是在线检查点。
	 * 这并不是特别关键，但由于我们可能会分配一个新的 TLI，使用关闭检查点允许我们遵循 TLI 仅在关闭检查点中更改的规则，这在 xlog_redo 中允许一些额外的错误检查。
	 *
	 * 在提升阶段，仅创建一个轻量级的恢复结束记录，而不是完整的检查点。检查点请求将在我们完全退出恢复模式并已开始接受查询后进行。
	 */
	if (ArchiveRecoveryRequested && IsUnderPostmaster &&
		PromoteIsTriggered())
	{
		fc_promoted = true;

		/*
		 * 插入一个特殊的 WAL 记录，以标记恢复的结束，因为我们
		 * 不在进行检查点。这意味着检查点进程可能正处于平滑的
		 * 重启点中，并且在这之后可能会持续数分钟。听起来很奇怪，
		 * 但效果大致相同，试图从重启点中出来然后再进行检查点将
		 * 更奇怪。无论如何，我们稍后会请求一个检查点以确保安全。
		 */
		fc_CreateEndOfRecoveryRecord();
	}
	else
	{
		RequestCheckpoint(CHECKPOINT_END_OF_RECOVERY |
						  CHECKPOINT_IMMEDIATE |
						  CHECKPOINT_WAIT);
	}

	return fc_promoted;
}

/*
 * 系统仍在恢复中吗？
 *
 * 与测试 InRecovery 不同，任何连接到共享内存的进程都可以使用。
 */
bool RecoveryInProgress(void)
{
	/*
	 * 我们每次检查共享状态，仅仅直到我们离开恢复模式。我们无法重新进入恢复状态，因此在共享变量被看到为假之后，就没有必要继续检查。
	 */
	if (!LocalRecoveryInProgress)
		return false;
	else
	{
		/*
		 * 使用易失性指针以确保我们对共享变量进行新读取。
		 */
		volatile XLogCtlData *fc_xlogctl = XLogCtl;

		LocalRecoveryInProgress = (fc_xlogctl->SharedRecoveryState != RECOVERY_STATE_DONE);

		/*
		 * 注意：当我们仍在恢复中时，不需要内存屏障。
		 * 我们可能会在返回后立即退出恢复，因此调用者不能指望“真”意味着我们仍在恢复中。
		 */

		return LocalRecoveryInProgress;
	}
}

/*
 * 从共享内存返回当前的恢复状态。
 *
 * 返回的状态与控制文件的内容保持一致。有关 RecoveryState 可取值的详细信息，请参见 xlog.h。
 */
RecoveryState
GetRecoveryState(void)
{
	RecoveryState fc_retval;

	SpinLockAcquire(&XLogCtl->info_lck);
	fc_retval = XLogCtl->SharedRecoveryState;
	SpinLockRelease(&XLogCtl->info_lck);

	return fc_retval;
}

/*
 * 这个进程是否允许插入新的 WAL 记录？
 *
 * 通常这基本上相当于 !RecoveryInProgress()。
 * 但我们也有规定可以在特定进程中强制结果为“真”或“假”，无论全局状态如何。
 */
bool XLogInsertAllowed(void)
{
	/*
	 * 如果值是“无条件为真”或“无条件为假”，则直接返回它。
	 * 这为我们提供了正常的快速路径，一旦已知恢复完成。
	 */
	if (LocalXLogInsertAllowed >= 0)
		return (bool) LocalXLogInsertAllowed;

	/*
	 * 否则，必须检查我们是否仍在恢复中。
	 */
	if (RecoveryInProgress())
		return false;

	/*
	 * 从恢复中退出时，重置为“无条件为真”，因为
	 * 不需要持续检查。
	 */
	LocalXLogInsertAllowed = 1;
	return true;
}

/*
 * 仅在当前进程中使 XLogInsertAllowed() 返回 true。
 *
 * 注意：可以在之后将 LocalXLogInsertAllowed 切换回 -1，
 * 甚至在那之后再次调用 LocalSetXLogInsertAllowed()。
 *
 * 返回 LocalXLogInsertAllowed 的先前值。
 */
static int fc_LocalSetXLogInsertAllowed(void)
{
	int			fc_oldXLogAllowed = LocalXLogInsertAllowed;

	LocalXLogInsertAllowed = 1;

	return fc_oldXLogAllowed;
}

/*
 * 从共享内存返回当前的重做指针。
 *
 * 作为副作用，本地 RedoRecPtr 副本会被更新。
 */
XLogRecPtr GetRedoRecPtr(void)
{
	XLogRecPtr	fc_ptr;

	/*
	 * XlogCtl 中可能未更新的副本就足够了。即使我们
	 * 抓取了 WAL 插入锁以读取 Insert->RedoRecPtr 中的权威值，
	 * 也可能在我们释放锁之后有人更新它。
	 */
	SpinLockAcquire(&XLogCtl->info_lck);
	fc_ptr = XLogCtl->RedoRecPtr;
	SpinLockRelease(&XLogCtl->info_lck);

	if (RedoRecPtr < fc_ptr)
		RedoRecPtr = fc_ptr;

	return RedoRecPtr;
}

/*
 * 返回在决定修改的块是否需要完整页面图像以
 * 包含在 WAL 记录中的信息。
 *
 * 返回的值是后端私有内存的缓存副本，并且可能过时或实际上未初始化，
 * 在这种情况下，它们将分别为 InvalidXLogRecPtr 和 false。
 * XLogInsertRecord 将在持有 WAL 插入锁时重新检查它们是否为最新值。
 */
void GetFullPageWriteInfo(XLogRecPtr *fc_RedoRecPtr_p, bool *fc_doPageWrites_p)
{
	*fc_RedoRecPtr_p = RedoRecPtr;
	*fc_doPageWrites_p = doPageWrites;
}

/*
 * GetInsertRecPtr -- 返回当前插入位置。
 *
 * 注意：实际返回的值是最后一个完整
 * xlog 页的位置信息。它比实际插入位置最多滞后 1 页。
 * 为此，我们不需要扫描 WAL 插入锁，近似值对于当前使用
 * 此函数是足够的。
 */
XLogRecPtr GetInsertRecPtr(void)
{
	XLogRecPtr	fc_recptr;

	SpinLockAcquire(&XLogCtl->info_lck);
	fc_recptr = XLogCtl->LogwrtRqst.Write;
	SpinLockRelease(&XLogCtl->info_lck);

	return fc_recptr;
}

/*
 * GetFlushRecPtr -- 返回当前刷新位置，即已知
 * 刷写到磁盘的最后 WAL 位置。这应该只在已知
 * 不在恢复中的系统上使用。
 */
XLogRecPtr GetFlushRecPtr(TimeLineID *fc_insertTLI)
{
	Assert(XLogCtl->SharedRecoveryState == RECOVERY_STATE_DONE);

	SpinLockAcquire(&XLogCtl->info_lck);
	LogwrtResult = XLogCtl->LogwrtResult;
	SpinLockRelease(&XLogCtl->info_lck);

	/*
	 * 如果我们正在写入和刷新 WAL，时间线就不能变化，因此
	 * 不需要锁。
	 */
	if (fc_insertTLI)
		*fc_insertTLI = XLogCtl->InsertTimeLineID;

	return LogwrtResult.Flush;
}

/*
 * GetWALInsertionTimeLine -- 返回不在恢复状态的系统的当前时间线。
 */
TimeLineID GetWALInsertionTimeLine(void)
{
	Assert(XLogCtl->SharedRecoveryState == RECOVERY_STATE_DONE);

	/* 由于值不能变化，因此不需要锁。 */
	return XLogCtl->InsertTimeLineID;
}

/*
 * GetLastImportantRecPtr -- 返回最后插入的重要记录的 LSN。
 * 所有未明确标记为不重要的记录均被视为重要记录。
 *
 * LSN 通过计算 WALInsertLocks[i].lastImportantAt 的最大值来确定。
 */
XLogRecPtr GetLastImportantRecPtr(void)
{
	XLogRecPtr	fc_res = InvalidXLogRecPtr;
	int			fc_i;

	for (fc_i = 0; fc_i < NUM_XLOGINSERT_LOCKS; fc_i++)
	{
		XLogRecPtr	fc_last_important;

		/*
		 * 需要加锁以防止 LSN 的撕裂读取，这在
		 * 一些受支持的平台上是可能的。WAL 插入锁仅
		 * 支持独占模式，因此我们必须使用它。
		 */
		LWLockAcquire(&WALInsertLocks[fc_i].l.lock, LW_EXCLUSIVE);
		fc_last_important = WALInsertLocks[fc_i].l.lastImportantAt;
		LWLockRelease(&WALInsertLocks[fc_i].l.lock);

		if (fc_res < fc_last_important)
			fc_res = fc_last_important;
	}

	return fc_res;
}

/*
 * 获取最后 xlog 段切换的时间和 LSN
 */
pg_time_t GetLastSegSwitchData(XLogRecPtr *fc_lastSwitchLSN)
{
	pg_time_t	fc_result;

	/* 需要 WALWriteLock，但共享锁就足够了 */
	LWLockAcquire(WALWriteLock, LW_SHARED);
	fc_result = XLogCtl->lastSegSwitchTime;
	*fc_lastSwitchLSN = XLogCtl->lastSegSwitchLSN;
	LWLockRelease(WALWriteLock);

	return fc_result;
}

/*
 * 在 postmaster 或独立后端关闭期间必须调用此函数一次
 */
void ShutdownXLOG(int fc_code, Datum fc_arg)
{
	/*
	 * 我们应该有一个辅助进程资源所有者可以使用，并且我们不应该
	 * 在某个其他资源所有者已安装的事务中。
	 */
	Assert(AuxProcessResourceOwner != NULL);
	Assert(CurrentResourceOwner == NULL ||
		   CurrentResourceOwner == AuxProcessResourceOwner);
	CurrentResourceOwner = AuxProcessResourceOwner;

	/* 不要在独立模式下喋喋不休 */
	ereport(IsPostmasterEnvironment ? LOG : NOTICE,
			(errmsg("shutting down")));

	/*
	 * 信号发送器移动到停止状态。
	 */
	WalSndInitStopping();

	/*
	 * 等待WAL发送器处于停止状态。这防止命令
	 * 写入新的WAL。
	 */
	WalSndWaitStopping();

	if (RecoveryInProgress())
		CreateRestartPoint(CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_IMMEDIATE);
	else
	{
		/*
		 * 如果启用了归档，请旋转最后一个XLOG文件，以便所有
		 * 剩余记录被归档（在关闭结束时，postmaster会再次唤醒归档
		 * 进程）。检查点记录将转到下一个XLOG文件，而不会被归档（暂时）。
		 */
		if (XLogArchivingActive())
			RequestXLogSwitch(false);

		CreateCheckPoint(CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_IMMEDIATE);
	}
}

/*
 * 记录检查点开始。
 */
static void fc_LogCheckpointStart(int fc_flags, bool fc_restartpoint)
{
	if (fc_restartpoint)
		ereport(LOG,
		/* 翻译者：占位符显示检查点选项 */
				(errmsg("restartpoint starting:%s%s%s%s%s%s%s%s",
						(fc_flags & CHECKPOINT_IS_SHUTDOWN) ? " shutdown" : "",
						(fc_flags & CHECKPOINT_END_OF_RECOVERY) ? " end-of-recovery" : "",
						(fc_flags & CHECKPOINT_IMMEDIATE) ? " immediate" : "",
						(fc_flags & CHECKPOINT_FORCE) ? " force" : "",
						(fc_flags & CHECKPOINT_WAIT) ? " wait" : "",
						(fc_flags & CHECKPOINT_CAUSE_XLOG) ? " wal" : "",
						(fc_flags & CHECKPOINT_CAUSE_TIME) ? " time" : "",
						(fc_flags & CHECKPOINT_FLUSH_ALL) ? " flush-all" : "")));
	else
		ereport(LOG,
		/* 翻译者：占位符显示检查点选项 */
				(errmsg("checkpoint starting:%s%s%s%s%s%s%s%s",
						(fc_flags & CHECKPOINT_IS_SHUTDOWN) ? " shutdown" : "",
						(fc_flags & CHECKPOINT_END_OF_RECOVERY) ? " end-of-recovery" : "",
						(fc_flags & CHECKPOINT_IMMEDIATE) ? " immediate" : "",
						(fc_flags & CHECKPOINT_FORCE) ? " force" : "",
						(fc_flags & CHECKPOINT_WAIT) ? " wait" : "",
						(fc_flags & CHECKPOINT_CAUSE_XLOG) ? " wal" : "",
						(fc_flags & CHECKPOINT_CAUSE_TIME) ? " time" : "",
						(fc_flags & CHECKPOINT_FLUSH_ALL) ? " flush-all" : "")));
}

/*
 * 记录检查点结束。
 */
static void fc_LogCheckpointEnd(bool fc_restartpoint)
{
	long		fc_write_msecs,
				fc_sync_msecs,
				fc_total_msecs,
				fc_longest_msecs,
				fc_average_msecs;
	uint64		fc_average_sync_time;

	CheckpointStats.ckpt_end_t = GetCurrentTimestamp();

	fc_write_msecs = TimestampDifferenceMilliseconds(CheckpointStats.ckpt_write_t,
												  CheckpointStats.ckpt_sync_t);

	fc_sync_msecs = TimestampDifferenceMilliseconds(CheckpointStats.ckpt_sync_t,
												 CheckpointStats.ckpt_sync_end_t);

	/* 累积检查点计时摘要数据，以毫秒为单位。 */
	PendingCheckpointerStats.checkpoint_write_time += fc_write_msecs;
	PendingCheckpointerStats.checkpoint_sync_time += fc_sync_msecs;

	/*
	 * 发布的所有计时统计数据都已被纳入计算。 只有在
	 * 要写入日志消息时，才继续。
	 */
	if (!log_checkpoints)
		return;

	fc_total_msecs = TimestampDifferenceMilliseconds(CheckpointStats.ckpt_start_t,
												  CheckpointStats.ckpt_end_t);

	/*
	 * CheckpointStats返回的计时值以微秒为单位。
	 * 转换为毫秒，以便于一致的打印。
	 */
	fc_longest_msecs = (long) ((CheckpointStats.ckpt_longest_sync + 999) / 1000);

	fc_average_sync_time = 0;
	if (CheckpointStats.ckpt_sync_rels > 0)
		fc_average_sync_time = CheckpointStats.ckpt_agg_sync_time /
			CheckpointStats.ckpt_sync_rels;
	fc_average_msecs = (long) ((fc_average_sync_time + 999) / 1000);

	if (fc_restartpoint)
		ereport(LOG,
				(errmsg("restartpoint complete: wrote %d buffers (%.1f%%); "
						"%d WAL file(s) added, %d removed, %d recycled; "
						"write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
						"sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
						"distance=%d kB, estimate=%d kB",
						CheckpointStats.ckpt_bufs_written,
						(double) CheckpointStats.ckpt_bufs_written * 100 / NBuffers,
						CheckpointStats.ckpt_segs_added,
						CheckpointStats.ckpt_segs_removed,
						CheckpointStats.ckpt_segs_recycled,
						fc_write_msecs / 1000, (int) (fc_write_msecs % 1000),
						fc_sync_msecs / 1000, (int) (fc_sync_msecs % 1000),
						fc_total_msecs / 1000, (int) (fc_total_msecs % 1000),
						CheckpointStats.ckpt_sync_rels,
						fc_longest_msecs / 1000, (int) (fc_longest_msecs % 1000),
						fc_average_msecs / 1000, (int) (fc_average_msecs % 1000),
						(int) (PrevCheckPointDistance / 1024.0),
						(int) (CheckPointDistanceEstimate / 1024.0))));
	else
		ereport(LOG,
				(errmsg("checkpoint complete: wrote %d buffers (%.1f%%); "
						"%d WAL file(s) added, %d removed, %d recycled; "
						"write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; "
						"sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; "
						"distance=%d kB, estimate=%d kB",
						CheckpointStats.ckpt_bufs_written,
						(double) CheckpointStats.ckpt_bufs_written * 100 / NBuffers,
						CheckpointStats.ckpt_segs_added,
						CheckpointStats.ckpt_segs_removed,
						CheckpointStats.ckpt_segs_recycled,
						fc_write_msecs / 1000, (int) (fc_write_msecs % 1000),
						fc_sync_msecs / 1000, (int) (fc_sync_msecs % 1000),
						fc_total_msecs / 1000, (int) (fc_total_msecs % 1000),
						CheckpointStats.ckpt_sync_rels,
						fc_longest_msecs / 1000, (int) (fc_longest_msecs % 1000),
						fc_average_msecs / 1000, (int) (fc_average_msecs % 1000),
						(int) (PrevCheckPointDistance / 1024.0),
						(int) (CheckPointDistanceEstimate / 1024.0))));
}

/*
 * 更新检查点之间距离的估算。
 *
 * 该估算用于计算要保持的WAL段的数量
 * 预分配，参见XLOGfileslop()。
 */
static void fc_UpdateCheckPointDistanceEstimate(uint64 fc_nbytes)
{
	/*
	 * 为了估算检查点之间消耗的段数，保留一个
	 * 移动平均值，用于前一个检查点周期生成的WAL量。 
	 * 然而，如果负载是突发性的，存在安静期和繁忙期，
	 * 我们希望能考虑到峰值负载。因此，与其使用简单的
	 * 移动平均，不如在前一个周期使用的WAL量小于
	 * 估算时，让平均值缓慢下降，但如果使用的
	 * 更多，则立即提升。
	 *
	 * 当通过max_wal_size触发检查点时，这应该收敛到
	 * CheckpointSegments * wal_segment_size,
	 *
	 * 注意：这并不关注是什么导致了检查点。
	 * 使用CHECKPOINT命令手动触发的检查点，或者例如
	 * 启动基备份时，引发的检查点与自动创建的
	 * 检查点计算相同。缓慢下降会在很大程度上掩盖它们，如果它们
	 * 不频繁。如果它们很频繁，计入这些检查点似乎是合理的；
	 * 如果你每5分钟发出一次手动检查点，而从不让定时检查点发生，
	 * 那么根据那个5分钟的间隔而不是设定的
	 * checkpoint_timeout进行预分配是合理的。
	 */
	PrevCheckPointDistance = fc_nbytes;
	if (CheckPointDistanceEstimate < fc_nbytes)
		CheckPointDistanceEstimate = fc_nbytes;
	else
		CheckPointDistanceEstimate =
			(0.90 * CheckPointDistanceEstimate + 0.10 * (double) fc_nbytes);
}

/*
 * 更新运行检查点的进程的ps显示。注意
 * 该例程不应进行任何分配，以便可以在关键部分
 * 被调用。
 */
static void fc_update_checkpoint_display(int fc_flags, bool fc_restartpoint, bool fc_reset)
{
	/*
	 * 仅在恢复结束和关闭
	 * 检查点或关闭重启点报告状态。更新ps显示
	 * 在这些情况下是有用的，因为可能无法依赖于
	 * pg_stat_activity查看检查点或启动
	 * 进程的状态。
	 */
	if ((fc_flags & (CHECKPOINT_END_OF_RECOVERY | CHECKPOINT_IS_SHUTDOWN)) == 0)
		return;

	if (fc_reset)
		set_ps_display("");
	else
	{
		char		fc_activitymsg[128];

		snprintf(fc_activitymsg, sizeof(fc_activitymsg), "performing %s%s%s",
				 (fc_flags & CHECKPOINT_END_OF_RECOVERY) ? "end-of-recovery " : "",
				 (fc_flags & CHECKPOINT_IS_SHUTDOWN) ? "shutdown " : "",
				 fc_restartpoint ? "restartpoint" : "checkpoint");
		set_ps_display(fc_activitymsg);
	}
}


/*
 * 执行检查点 --- 可能是在关闭期间或动态进行
 *
 * flags 是以下标志的按位或：
 *	CHECKPOINT_IS_SHUTDOWN：检查点用于数据库关闭。
 *	CHECKPOINT_END_OF_RECOVERY：检查点用于 WAL 恢复结束。
 *	CHECKPOINT_IMMEDIATE：尽快完成检查点，
 *		忽略 checkpoint_completion_target 参数。
 *	CHECKPOINT_FORCE：强制进行检查点，即使自上一个检查点以来没有发生 XLOG 活动
 *		（由 CHECKPOINT_IS_SHUTDOWN 或
 *		CHECKPOINT_END_OF_RECOVERY 隐含）。
 *	CHECKPOINT_FLUSH_ALL：还刷新未记录表的缓冲区。
 *
 * 注意：flags 包含其他位，仅对日志记录目的感兴趣。
 * 特别注意，这个例程是同步的，不考虑 CHECKPOINT_WAIT。
 *
 * 如果 !shutdown，那么我们正在写入在线检查点。这是一种非常特殊的
 * 操作和 WAL 记录，因为检查点操作在一段时间内发生，但在逻辑上
 * 只发生在单个 LSN。WAL 记录的逻辑位置（重做指针）与物理位置相同或更早。
 * 当我们重放 WAL 时，我们通过其物理位置找到检查点，然后读取重做指针
 * 并实际上在早期的逻辑位置开始重放。请注意，我们在逻辑位置上
 * 不写入 *任何* 内容，因此该位置可以是任何其他类型的 WAL 记录。
 * 所有这些机制使我们能够在进行检查点时继续工作。
 * 因此，动作的时机在这里至关重要，请注意，这个函数在繁忙系统上执行
 * 可能需要几分钟。
 */
void CreateCheckPoint(int fc_flags)
{
	bool		fc_shutdown;
	CheckPoint	fc_checkPoint;
	XLogRecPtr	fc_recptr;
	XLogSegNo	fc__logSegNo;
	XLogCtlInsert *fc_Insert = &XLogCtl->Insert;
	uint32		fc_freespace;
	XLogRecPtr	fc_PriorRedoPtr;
	XLogRecPtr	fc_curInsert;
	XLogRecPtr	fc_last_important_lsn;
	VirtualTransactionId *fc_vxids;
	int			fc_nvxids;
	int			fc_oldXLogAllowed = 0;

	/*
	 * 恢复结束检查点实际上是关闭检查点，只是在不同的时间发出。
	 */
	if (fc_flags & (CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_END_OF_RECOVERY))
		fc_shutdown = true;
	else
		fc_shutdown = false;

	/* 计算前一个数据项的长度 */
	if (RecoveryInProgress() && (fc_flags & CHECKPOINT_END_OF_RECOVERY) == 0)
		elog(ERROR, "can't create a checkpoint during recovery");

#ifdef  FDD
	/*
	 * 在阻止 WAL 写入之前刷新脏复制槽，以便
	 * 任何故障转移槽被写出。
	 */
	CheckPointReplicationSlots();
#endif

	/*
	 * 准备收集统计信息。
	 *
	 * 注意：因为在检查点进行时 log_checkpoints 可能会改变，
	 * 所以我们总是累积统计信息，即使
	 * log_checkpoints 当前关闭。
	 */
	MemSet(&CheckpointStats, 0, sizeof(CheckpointStats));
	CheckpointStats.ckpt_start_t = GetCurrentTimestamp();

	/*
	 * 让 smgr 为检查点做好准备；这必须在
	 * 临界区外发生，并在我们确定 REDO 指针之前。请注意，
	 * smgr 不能做任何一旦决定不需要检查点就必须撤销的事情。
	 */
	SyncPreCheckpoint();

	/*
	 * 使用临界区强制系统崩溃，如果我们有问题。
	 */
	START_CRIT_SECTION();

	if (fc_shutdown)
	{
		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
		ControlFile->state = DB_SHUTDOWNING;
		fc_UpdateControlFile();
		LWLockRelease(ControlFileLock);
	}

	/* 开始填写检查点 WAL 记录 */
	MemSet(&fc_checkPoint, 0, sizeof(fc_checkPoint));
	fc_checkPoint.time = (pg_time_t) time(NULL);

	/*
	 * 对于热备份，在修复重做
	 * 指针之前推导出 oldestActiveXid。这使我们能够开始累积更改以组装我们的
	 * 锁和事务的起始快照。
	 */
	if (!fc_shutdown && XLogStandbyInfoActive())
		fc_checkPoint.oldestActiveXid = GetOldestActiveTransactionId();
	else
		fc_checkPoint.oldestActiveXid = InvalidTransactionId;

	/*
	 * 获取获取插入锁之前最后一个重要记录的位置（因为
	 * GetLastImportantRecPtr() 也会锁定 WAL 锁）。
	 */
	fc_last_important_lsn = GetLastImportantRecPtr();

	/*
	 * 在检查插入状态以确定检查点 REDO 指针时，
	 * 我们必须阻止并发插入。
	 */
	fc_WALInsertLockAcquireExclusive();
	fc_curInsert = fc_XLogBytePosToRecPtr(fc_Insert->CurrBytePos);

	/*
	 * 如果这不是关机或强制检查点，并且没有需要检查点的WAL活动，则跳过它。这里的想法是
	 * 在系统空闲时避免插入重复的检查点。
	 */
	if ((fc_flags & (CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_END_OF_RECOVERY |
				  CHECKPOINT_FORCE)) == 0)
	{
		if (fc_last_important_lsn == ControlFile->checkPoint)
		{
			fc_WALInsertLockRelease();
			END_CRIT_SECTION();
			ereport(DEBUG1,
					(errmsg_internal("checkpoint skipped because system is idle")));
			return;
		}
	}

	/*
	 * 在任何人被允许写入WAL之前，会创建一个恢复结束检查点。为了允许我们写入检查点记录，
	 * 暂时启用XLogInsertAllowed。
	 */
	if (fc_flags & CHECKPOINT_END_OF_RECOVERY)
		fc_oldXLogAllowed = fc_LocalSetXLogInsertAllowed();

	fc_checkPoint.ThisTimeLineID = XLogCtl->InsertTimeLineID;
	if (fc_flags & CHECKPOINT_END_OF_RECOVERY)
		fc_checkPoint.PrevTimeLineID = XLogCtl->PrevTimeLineID;
	else
		fc_checkPoint.PrevTimeLineID = fc_checkPoint.ThisTimeLineID;

	fc_checkPoint.fullPageWrites = fc_Insert->fullPageWrites;

	/*
	 * 计算新的REDO记录指针 = 下一个XLOG记录的位置。
	 *
	 * 注意：这不一定是检查点记录本身的位置，因为其他后端可能在我们执行
	 * 缓冲区刷新工作时插入更多的XLOG记录。那些XLOG记录在逻辑上是在
	 * 检查点之后，尽管在物理上是在之前。明白了吗？
	 */
	fc_freespace = INSERT_FREESPACE(fc_curInsert);
	if (fc_freespace == 0)
	{
		if (XLogSegmentOffset(fc_curInsert, wal_segment_size) == 0)
			fc_curInsert += SizeOfXLogLongPHD;
		else
			fc_curInsert += SizeOfXLogShortPHD;
	}
	fc_checkPoint.redo = fc_curInsert;

	/*
	 * 在这里我们更新共享的RedoRecPtr以用于未来的XLogInsert调用；此操作
	 * 必须在持有所有插入锁的情况下进行。
	 *
	 * 注意：如果我们未能完成检查点，RedoRecPtr将指向它实际上需要指向的
	 * 位置之后。这是可以的；唯一的后果是XLogInsert可能会备份整个缓冲区，
	 * 而它实际上并不需要这样。我们不能推迟推进RedoRecPtr，因为
	 * 在我们转储缓冲区时发生的XLogInserts必须假设它们的缓冲区更改没有
	 * 包含在检查点中。
	 */
	RedoRecPtr = XLogCtl->Insert.RedoRecPtr = fc_checkPoint.redo;

	/*
	 * 现在我们可以释放WAL插入锁，允许其他事务在我们刷新磁盘缓冲区时继续。
	 */
	fc_WALInsertLockRelease();

	/* 更新受info_lck保护的RedoRecPtr副本 */
	SpinLockAcquire(&XLogCtl->info_lck);
	XLogCtl->RedoRecPtr = fc_checkPoint.redo;
	SpinLockRelease(&XLogCtl->info_lck);

	/*
	 * 如果启用，记录检查点开始。我们将此推迟到现在，以便在决定跳过检查点时
	 * 不记录任何内容。
	 */
	if (log_checkpoints)
		fc_LogCheckpointStart(fc_flags, false);

	/* 更新进程标题 */
	fc_update_checkpoint_display(fc_flags, false, false);

	TRACE_POSTGRESQL_CHECKPOINT_START(flags);

	/*
	 * 获取检查点记录所需的其他信息。
	 *
	 * 我们不需要在检查点中保存oldestClogXid，它仅在clog被截断的短暂期间内
	 * 有意义，如果我们在那期间崩溃，稍后将重新执行clog截断并修复那里
	 * 的oldestClogXid。
	 */
	LWLockAcquire(XidGenLock, LW_SHARED);
	fc_checkPoint.nextXid = ShmemVariableCache->nextXid;
	fc_checkPoint.oldestXid = ShmemVariableCache->oldestXid;
	fc_checkPoint.oldestXidDB = ShmemVariableCache->oldestXidDB;
	LWLockRelease(XidGenLock);

	LWLockAcquire(CommitTsLock, LW_SHARED);
	fc_checkPoint.oldestCommitTsXid = ShmemVariableCache->oldestCommitTsXid;
	fc_checkPoint.newestCommitTsXid = ShmemVariableCache->newestCommitTsXid;
	LWLockRelease(CommitTsLock);

	LWLockAcquire(OidGenLock, LW_SHARED);
	fc_checkPoint.nextOid = ShmemVariableCache->nextOid;
	if (!fc_shutdown)
		fc_checkPoint.nextOid += ShmemVariableCache->oidCount;
	LWLockRelease(OidGenLock);

	MultiXactGetCheckptMulti(fc_shutdown,
							 &fc_checkPoint.nextMulti,
							 &fc_checkPoint.nextMultiOffset,
							 &fc_checkPoint.oldestMulti,
							 &fc_checkPoint.oldestMultiDB);

	/*
	 * 在构建检查点记录后，确保所有共享内存磁盘缓冲区
	 * 和提交日志缓冲区已刷新到磁盘。
	 *
	 * 此I/O可能由于各种原因失败。如果是这样，我们将无法
	 * 完成检查点，但没有理由强制系统出现恐慌。因此，在执行
	 * 这个操作时，退出临界区。
	 */
	END_CRIT_SECTION();

	/*
	 * 在某些情况下，有一组操作必须全部在某个检查点记录的某一侧发生。在刷新检查点记录之前，我们必须显式等待任何当前正在执行这些操作组的后端。
	 *
	 * 一个例子是事务结束，因此我们必须等待任何当前处于提交关键区域的事务。如果一个事务在 REDO 点之前插入了它的提交记录到 XLOG 中，那么从 REDO 点崩溃恢复将不会重放该记录，这意味着我们的刷新最好包括该事务对 pg_xact 的更新。因此，我们在继续之前要等到它离开其提交关键区域。详见 RecordTransactionCommit() 中的说明。
	 *
	 * 由于我们已经释放了插入锁，因此这个测试有点模糊：我们可能会等待一些其实并不需要等待的事务。但延迟应该很短，而且似乎让检查点稍微延长一点时间比阻止插入超过必要时间要好。（事实上，我们遇到这个问题的原因是 xact.c 将提交记录 XLOG 插入和 clog 更新作为两个不同的步骤，由不同的锁保护，但从最小化锁争用的角度来看，这样做似乎更好。）
	 *
	 * 当我们检查时，尚未设置 delayChkptFlags 的事务不会有风险，因为它还没有插入其提交记录；而已经清除该标志的事务也没有风险，因为它已经完成了 clog 的修复，我们会正确刷新下面的更新。因此，我们不会错过任何需要等待的事务。
	 */
	fc_vxids = GetVirtualXIDsDelayingChkpt(&fc_nvxids, DELAY_CHKPT_START);
	if (fc_nvxids > 0)
	{
		do
		{
			/*
			 * 在我们等待的同时继续吸收 fsync 请求。如果不这样做，可能会出现死锁，如果阻止检查点的进程试图向队列添加请求。
			 */
			AbsorbSyncRequests();
			pg_usleep(10000L);	/* 等待 10 毫秒 */
		} while (HaveVirtualXIDsDelayingChkpt(fc_vxids, fc_nvxids,
											  DELAY_CHKPT_START));
	}
	pfree(fc_vxids);

	fc_CheckPointGuts(fc_checkPoint.redo, fc_flags);

	fc_vxids = GetVirtualXIDsDelayingChkpt(&fc_nvxids, DELAY_CHKPT_COMPLETE);
	if (fc_nvxids > 0)
	{
		do
		{
			AbsorbSyncRequests();
			pg_usleep(10000L);	/* 等待 10 毫秒 */
		} while (HaveVirtualXIDsDelayingChkpt(fc_vxids, fc_nvxids,
											  DELAY_CHKPT_COMPLETE));
	}
	pfree(fc_vxids);

	/*
	 * 获取正在运行的事务快照并将其写入 WAL。这允许我们在存档恢复期间重建正在运行的事务状态（如有必要）。如果此信息被禁用，则跳过。
	 *
	 * 如果我们正在关闭，或者启动进程正在完成崩溃恢复，我们不需要写入正在运行的 xact 数据。
	 */
	if (!fc_shutdown && XLogStandbyInfoActive())
		LogStandbySnapshot();

	START_CRIT_SECTION();

	/*
	 * 现在将检查点记录插入到 XLOG 中。
	 */
	XLogBeginInsert();
	XLogRegisterData((char *) (&fc_checkPoint), sizeof(fc_checkPoint));
	fc_recptr = XLogInsert(RM_XLOG_ID,
						fc_shutdown ? XLOG_CHECKPOINT_SHUTDOWN :
						XLOG_CHECKPOINT_ONLINE);

	XLogFlush(fc_recptr);

	/*
	 * 在关闭检查点后，我们不能写入任何新的 WAL，否则在下次启动时将被覆盖。没有人甚至应该尝试，这只是允许进行一致性检查。在恢复结束的检查点情况下，我们希望临时禁用写入，直到系统退出恢复。
	 */
	if (fc_shutdown)
	{
		if (fc_flags & CHECKPOINT_END_OF_RECOVERY)
			LocalXLogInsertAllowed = fc_oldXLogAllowed;
		else
			LocalXLogInsertAllowed = 0; /* 永远不再写入 WAL */
	}

	/*
	 * 我们现在有 ProcLastRecPtr = 实际检查点记录的开始，recptr = 实际检查点记录的结束。
	 */
	if (fc_shutdown && fc_checkPoint.redo != ProcLastRecPtr)
		ereport(PANIC,
				(errmsg("concurrent write-ahead log activity while database system is shutting down")));

	/*
	 * 记住之前检查点的 redo 指针，以便用于
	 * UpdateCheckPointDistanceEstimate()
	 */
	fc_PriorRedoPtr = ControlFile->checkPointCopy.redo;

	/*
	 * 更新控制文件。
	 */
	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	if (fc_shutdown)
		ControlFile->state = DB_SHUTDOWNED;
	ControlFile->checkPoint = ProcLastRecPtr;
	ControlFile->checkPointCopy = fc_checkPoint;
	/* 崩溃恢复应该始终恢复到WAL的末尾 */
	ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
	ControlFile->minRecoveryPointTLI = 0;

	/*
	 * 持久化未记录的LSN值。它在崩溃恢复时重置，因此在非关闭检查点上未使用，但出于调试目的似乎总是存储它是有用的。
	 */
	SpinLockAcquire(&XLogCtl->ulsn_lck);
	ControlFile->unloggedLSN = XLogCtl->unloggedLSN;
	SpinLockRelease(&XLogCtl->ulsn_lck);

	fc_UpdateControlFile();
	LWLockRelease(ControlFileLock);

	/* 更新检查点XID/epoch的共享内存副本 */
	SpinLockAcquire(&XLogCtl->info_lck);
	XLogCtl->ckptFullXid = fc_checkPoint.nextXid;
	SpinLockRelease(&XLogCtl->info_lck);

	/*
	 * 我们现在完成了关键更新；如果我们在处理旧的日志段时遇到麻烦，则无需系统恐慌。
	 */
	END_CRIT_SECTION();

	/*
	 * 让smgr进行检查点后的清理（例如，删除旧文件）。
	 */
	SyncPostCheckpoint();

	/*
	 * 如果先前的检查点存在，则更新检查点之间的平均距离。
	 */
	if (fc_PriorRedoPtr != InvalidXLogRecPtr)
		fc_UpdateCheckPointDistanceEstimate(RedoRecPtr - fc_PriorRedoPtr);

	/*
	 * 删除旧的日志文件，那些不再需要的文件，以防止持有xlog的磁盘填满。
	 */
	XLByteToSeg(RedoRecPtr, fc__logSegNo, wal_segment_size);
	fc_KeepLogSeg(fc_recptr, &fc__logSegNo);
	if (InvalidateObsoleteReplicationSlots(fc__logSegNo))
	{
		/*
		 * 一些插槽已被失效；重新计算旧段的地平线，从RedoRecPtr重新开始。
		 */
		XLByteToSeg(RedoRecPtr, fc__logSegNo, wal_segment_size);
		fc_KeepLogSeg(fc_recptr, &fc__logSegNo);
	}
	fc__logSegNo--;
	fc_RemoveOldXlogFiles(fc__logSegNo, RedoRecPtr, fc_recptr,
					   fc_checkPoint.ThisTimeLineID);

	/*
	 * 如果需要，生成更多的日志段。（在回收旧的日志段之后执行此操作，因为那可能会提供一些所需的文件。）
	 */
	if (!fc_shutdown)
		fc_PreallocXlogFiles(fc_recptr, fc_checkPoint.ThisTimeLineID);

	/*
	 * 如果可能，截断pg_subtrans。我们可以丢弃所有在任何正在运行的事务的最旧XMIN之前的数据。以后没有事务会尝试引用任何比那更旧的pg_subtrans条目（参见subtrans.c中的断言）。不过在恢复过程中，我们不能这样做，因为尚未调用StartupSUBTRANS。
	 */
	if (!RecoveryInProgress())
		TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning());

	/* 实际工作完成；记录并更新统计信息。 */
	fc_LogCheckpointEnd(false);

	/* 重置进程标题 */
	fc_update_checkpoint_display(fc_flags, false, true);

	TRACE_POSTGRESQL_CHECKPOINT_DONE(CheckpointStats.ckpt_bufs_written,
									 NBuffers,
									 CheckpointStats.ckpt_segs_added,
									 CheckpointStats.ckpt_segs_removed,
									 CheckpointStats.ckpt_segs_recycled);
}

/*
 * 在WAL中标记恢复的结束，尽管没有运行完整的检查点。我们可以预期在执行此操作时，恢复点可能正在进行中，尽管我们不愿意等待它完成。
 *
 * CreateRestartPoint()允许恢复可能在重启点完成之前结束的情况，因此没有并发行为的担忧。
 */
static void fc_CreateEndOfRecoveryRecord(void)
{
	xl_end_of_recovery fc_xlrec;
	XLogRecPtr	fc_recptr;

	/* 计算前一个数据项的长度 */
	if (!RecoveryInProgress())
		elog(ERROR, "can only be used to end recovery");

	fc_xlrec.end_time = GetCurrentTimestamp();

	fc_WALInsertLockAcquireExclusive();
	fc_xlrec.ThisTimeLineID = XLogCtl->InsertTimeLineID;
	fc_xlrec.PrevTimeLineID = XLogCtl->PrevTimeLineID;
	fc_WALInsertLockRelease();

	START_CRIT_SECTION();

	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec, sizeof(xl_end_of_recovery));
	fc_recptr = XLogInsert(RM_XLOG_ID, XLOG_END_OF_RECOVERY);

	XLogFlush(fc_recptr);

	/*
	 * 更新控制文件，以便崩溃恢复可以跟踪此时的时间线变化。
	 */
	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	ControlFile->minRecoveryPoint = fc_recptr;
	ControlFile->minRecoveryPointTLI = fc_xlrec.ThisTimeLineID;
	fc_UpdateControlFile();
	LWLockRelease(ControlFileLock);

	END_CRIT_SECTION();
}

/*
 * 写入一个 OVERWRITE_CONTRECORD 消息。
 *
 * 在 WAL 重播时，我们期望在页面的开始处有一个续记录，
 * 但实际上没有，这时恢复结束，WAL 写入在该点恢复。
 * 但是，在破损记录的起始位置恢复写入新的 WAL 是错误的，
 * 因为下游的 WAL 消费者（物理副本）并没有准备好“回放”。
 * 因此，在完成所有有效 WAL 的重播后，必须首先在缺少 contrecord 的点
 * 写入这种类型的记录；为了支持 xlogreader 检测这个特殊情况，
 * XLP_FIRST_IS_OVERWRITE_CONTRECORD 也被添加到记录发生的页面头中。
 * xlogreader 有一种权宜之计机制来报告关于破损记录的元数据，
 * 这就是我们在这里使用的。
 *
 * 在重播时，XLP_FIRST_IS_OVERWRITE_CONTRECORD 指示 xlogreader
 * 跳过它正在读取的记录，并返回被跳过记录的 LSN，
 * 以便它的调用者可以验证（在“重播”该记录时）XLOG_OVERWRITE_CONTRECORD
 * 是否与实际上被覆盖的内容匹配。
 *
 * 'aborted_lsn' 是不完整记录的起始位置。
 * 它包含在 WAL 记录中。'pagePtr' 和 'newTLI' 指向
 * 要插入记录的 XLOG 页的起始位置。它们必须与当前的 WAL
 * 插入位置匹配，这里传递它们只是为了验证这一点。
 */
static XLogRecPtr fc_CreateOverwriteContrecordRecord(XLogRecPtr fc_aborted_lsn, XLogRecPtr fc_pagePtr,
								TimeLineID fc_newTLI)
{
	xl_overwrite_contrecord fc_xlrec;
	XLogRecPtr	fc_recptr;
	XLogPageHeader fc_pagehdr;
	XLogRecPtr	fc_startPos;

	/* 运行检查 */
	if (!RecoveryInProgress())
		elog(ERROR, "can only be used at end of recovery");
	if (fc_pagePtr % XLOG_BLCKSZ != 0)
		elog(ERROR, "invalid position for missing continuation record %X/%X",
			 LSN_FORMAT_ARGS(fc_pagePtr));

	/* 当前的 WAL 插入位置应该在页面头之后 */
	fc_startPos = fc_pagePtr;
	if (XLogSegmentOffset(fc_startPos, wal_segment_size) == 0)
		fc_startPos += SizeOfXLogLongPHD;
	else
		fc_startPos += SizeOfXLogShortPHD;
	fc_recptr = GetXLogInsertRecPtr();
	if (fc_recptr != fc_startPos)
		elog(ERROR, "invalid WAL insert position %X/%X for OVERWRITE_CONTRECORD",
			 LSN_FORMAT_ARGS(fc_recptr));

	START_CRIT_SECTION();

	/*
	 * 初始化 XLOG 页面头（通过 GetXLogBuffer），并设置
	 * XLP_FIRST_IS_OVERWRITE_CONTRECORD 标志。
	 *
	 * 目前不允许其他后端写入 WAL，因此获取 WAL
	 * 插入锁只是形式上的。
	 */
	fc_WALInsertLockAcquire();
	fc_pagehdr = (XLogPageHeader) fc_GetXLogBuffer(fc_pagePtr, fc_newTLI);
	fc_pagehdr->xlp_info |= XLP_FIRST_IS_OVERWRITE_CONTRECORD;
	fc_WALInsertLockRelease();

	/*
	 * 将 XLOG_OVERWRITE_CONTRECORD 记录作为页面上的第一个记录插入。
	 * 我们知道它成为第一个记录，因为没有其他后端
	 * 允许写入 WAL。
	 */
	XLogBeginInsert();
	fc_xlrec.overwritten_lsn = fc_aborted_lsn;
	fc_xlrec.overwrite_time = GetCurrentTimestamp();
	XLogRegisterData((char *) &fc_xlrec, sizeof(xl_overwrite_contrecord));
	fc_recptr = XLogInsert(RM_XLOG_ID, XLOG_OVERWRITE_CONTRECORD);

	/* 检查记录是否插入到正确的位置 */
	if (ProcLastRecPtr != fc_startPos)
		elog(ERROR, "OVERWRITE_CONTRECORD was inserted to unexpected position %X/%X",
			 LSN_FORMAT_ARGS(ProcLastRecPtr));

	XLogFlush(fc_recptr);

	END_CRIT_SECTION();

	return fc_recptr;
}

/*
 * 将共享内存中的所有数据刷新到磁盘，并执行 fsync
 *
 * 这是常规检查点和恢复重启点之间共享的公共代码。
 */
static void fc_CheckPointGuts(XLogRecPtr fc_checkPointRedo, int fc_flags)
{
	CheckPointRelationMap();
	CheckPointReplicationSlots();
	CheckPointSnapBuild();
	CheckPointLogicalRewriteHeap();
	CheckPointReplicationOrigin();

	/* 将所有脏数据写出到 SLRUs 和主缓冲池 */
	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
	CheckpointStats.ckpt_write_t = GetCurrentTimestamp();
	CheckPointCLOG();
	CheckPointCommitTs();
	CheckPointSUBTRANS();
	CheckPointMultiXact();
	CheckPointPredicate();
	CheckPointBuffers(fc_flags);

	/* 执行所有排队的 fsync 操作 */
	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_SYNC_START();
	CheckpointStats.ckpt_sync_t = GetCurrentTimestamp();
	ProcessSyncRequests();
	CheckpointStats.ckpt_sync_end_t = GetCurrentTimestamp();
	TRACE_POSTGRESQL_BUFFER_CHECKPOINT_DONE();

	/* 我们故意将 2PC 检查点延迟到尽可能长的时间 */
	CheckPointTwoPhase(fc_checkPointRedo);
}

/*
 * 如果适当，保存恢复重启的检查点
 *
 * 每次从 XLOG 读取检查点记录时都会调用这个函数。
 * 它必须确定检查点是否代表一个安全的重启点。
 * 如果是，该检查点记录将存储在共享内存中，以便
 * CreateRestartPoint 可以查询它。 （注意，后一个函数
 * 是由检查点进程执行的，而这个函数将由启动过程执行。）
 */
static void fc_RecoveryRestartPoint(const CheckPoint *fc_checkPoint, XLogReaderState *fc_record)
{
	/*
	 * 如果我们看到任何对不存在页面的引用，也应避免创建重启点。
	 * 从重启点重新启动恢复将无法看到这些引用，因此我们将失去交叉检查，
	 * 以确保这些页面属于后期删除的关系。
	 */
	if (XLogHaveInvalidPages())
	{
		elog(trace_recovery(DEBUG2),
			 "could not record restart point at %X/%X because there "
			 "are unresolved references to invalid pages",
			 LSN_FORMAT_ARGS(fc_checkPoint->redo));
		return;
	}

	/*
	 * 将检查点记录复制到共享内存中，以便检查点可以计算
	 * 下次想要执行重启点的时间。
	 */
	SpinLockAcquire(&XLogCtl->info_lck);
	XLogCtl->lastCheckPointRecPtr = fc_record->ReadRecPtr;
	XLogCtl->lastCheckPointEndPtr = fc_record->EndRecPtr;
	XLogCtl->lastCheckPoint = *fc_checkPoint;
	SpinLockRelease(&XLogCtl->info_lck);
}

/*
 * 在可能的情况下建立一个重启点。
 *
 * 这类似于CreateCheckPoint，但在WAL恢复期间使用，
 * 以建立一个恢复可以向前推进的点，而无需重放整个恢复日志。
 *
 * 如果建立了新的重启点则返回true。我们只能在自上次重启点以来
 * 重放了安全检查点记录后才能建立重启点。
 */
bool CreateRestartPoint(int fc_flags)
{
	XLogRecPtr	fc_lastCheckPointRecPtr;
	XLogRecPtr	fc_lastCheckPointEndPtr;
	CheckPoint	fc_lastCheckPoint;
	XLogRecPtr	fc_PriorRedoPtr;
	XLogRecPtr	fc_receivePtr;
	XLogRecPtr	fc_replayPtr;
	TimeLineID	fc_replayTLI;
	XLogRecPtr	fc_endptr;
	XLogSegNo	fc__logSegNo;
	TimestampTz fc_xtime;

	/* 不能同时发生并发检查点/重启点 */
	Assert(!IsUnderPostmaster || MyBackendType == B_CHECKPOINTER);
	
#ifdef FDD
	CheckPointReplicationSlots();
#endif

	/* 获取最后一个安全检查点记录的本地副本。 */
	SpinLockAcquire(&XLogCtl->info_lck);
	fc_lastCheckPointRecPtr = XLogCtl->lastCheckPointRecPtr;
	fc_lastCheckPointEndPtr = XLogCtl->lastCheckPointEndPtr;
	fc_lastCheckPoint = XLogCtl->lastCheckPoint;
	SpinLockRelease(&XLogCtl->info_lck);

	/*
	 * 检查我们是否仍在恢复模式中。如果在此检查后退出恢复模式是可以的，
	 * 重启点仍然有效。
	 */
	if (!RecoveryInProgress())
	{
		ereport(DEBUG2,
				(errmsg_internal("skipping restartpoint, recovery has already ended")));
		return false;
	}

	/*
	 * 如果我们重放的最后一个检查点记录已经是我们的最后一个
	 * 重启点，则无法执行新的重启点。在这种情况下我们仍然
	 * 更新minRecoveryPoint，以便如果这是一个关闭重启点，
	 * 我们不会在比之前更早的时间启动。这不是严格必要的，
	 * 但当热备用启用时，如果数据库在上次关闭之前的某个时间点
	 * 开放只读连接会显得相当奇怪。不过在立即关闭的情况下，
	 * 这种时间旅行仍然是可能的。
	 *
	 * 创建重启点时，我们不会显式推进minRecoveryPoint。
	 * 在刷新缓冲区时，将假定这会作为副作用完成。
	 */
	if (XLogRecPtrIsInvalid(fc_lastCheckPointRecPtr) ||
		fc_lastCheckPoint.redo <= ControlFile->checkPointCopy.redo)
	{
		ereport(DEBUG2,
				(errmsg_internal("skipping restartpoint, already performed at %X/%X",
								 LSN_FORMAT_ARGS(fc_lastCheckPoint.redo))));

		fc_UpdateMinRecoveryPoint(InvalidXLogRecPtr, true);
		if (fc_flags & CHECKPOINT_IS_SHUTDOWN)
		{
			LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
			ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
			fc_UpdateControlFile();
			LWLockRelease(ControlFileLock);
		}
		return false;
	}

	/*
	 * 更新共享的RedoRecPtr，以便启动进程可以计算自上次重启点以来
	 * 重放的段数，并在超过CheckPointSegments时请求重启点。
	 *
	 * 就像在CreateCheckPoint()中一样，推迟插入以更新它，
	 * 尽管在恢复过程中这仅仅是表面上的，因为没有WAL插入发生。
	 */
	fc_WALInsertLockAcquireExclusive();
	RedoRecPtr = XLogCtl->Insert.RedoRecPtr = fc_lastCheckPoint.redo;
	fc_WALInsertLockRelease();

	/* 还更新info_lck保护的副本 */
	SpinLockAcquire(&XLogCtl->info_lck);
	XLogCtl->RedoRecPtr = fc_lastCheckPoint.redo;
	SpinLockRelease(&XLogCtl->info_lck);

	/*
	 * 准备收集统计信息。
	 *
	 * 注意：因为在检查点进行时 log_checkpoints 可能会改变，
	 * 所以我们总是累积统计信息，即使
	 * log_checkpoints 当前关闭。
	 */
	MemSet(&CheckpointStats, 0, sizeof(CheckpointStats));
	CheckpointStats.ckpt_start_t = GetCurrentTimestamp();

	if (log_checkpoints)
		fc_LogCheckpointStart(fc_flags, true);

	/* 更新进程标题 */
	fc_update_checkpoint_display(fc_flags, true, false);

	fc_CheckPointGuts(fc_lastCheckPoint.redo, fc_flags);

	/*
	 * 记住之前检查点的 redo 指针，以便用于
	 * UpdateCheckPointDistanceEstimate()
	 */
	fc_PriorRedoPtr = ControlFile->checkPointCopy.redo;

	/*
	 * 使用当前时间更新pg_control。检查它是否仍然显示
	 * 较旧的检查点，否则不执行任何操作；这是一个快速的黑客，
	 * 以确保如果我们以某种方式在恢复结束检查点后到达这里，
	 * 不会发生什么严重的问题。
	 */
	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	if (ControlFile->checkPointCopy.redo < fc_lastCheckPoint.redo)
	{
		/*
		 * 更新检查点信息。即使集群
		 * 没有显示 DB_IN_ARCHIVE_RECOVERY，我们仍然这样做，以匹配下面回收的一组 WAL
		 * 段。
		 */
		ControlFile->checkPoint = fc_lastCheckPointRecPtr;
		ControlFile->checkPointCopy = fc_lastCheckPoint;

		/*
		 * 确保 minRecoveryPoint 超过检查点记录，并在控制文件仍然显示 DB_IN_ARCHIVE_RECOVERY 的情况下更新它。
		 * 通常，这在写出脏缓冲区时已经发生，
		 * 但不一定 - 例如，因为没有脏化缓冲区。我们这样做是因为在恢复中执行的备份使用 minRecoveryPoint
		 * 来确定必须包含哪些 WAL 文件，并且包含检查点记录的文件（或文件）必须
		 * 至少被包含。请注意，对于恢复的普通重启，
		 * 让最小恢复点在这之前没有任何价值，因为重做将在检查点记录之后开始。
		 */
		if (ControlFile->state == DB_IN_ARCHIVE_RECOVERY)
		{
			if (ControlFile->minRecoveryPoint < fc_lastCheckPointEndPtr)
			{
				ControlFile->minRecoveryPoint = fc_lastCheckPointEndPtr;
				ControlFile->minRecoveryPointTLI = fc_lastCheckPoint.ThisTimeLineID;

				/* 更新本地副本 */
				LocalMinRecoveryPoint = ControlFile->minRecoveryPoint;
				LocalMinRecoveryPointTLI = ControlFile->minRecoveryPointTLI;
			}
			if (fc_flags & CHECKPOINT_IS_SHUTDOWN)
				ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
		}
		fc_UpdateControlFile();
	}
	LWLockRelease(ControlFileLock);

	/*
	 * 如果存在之前的检查点，则更新检查点/重启点之间的平均距离。
	 */
	if (fc_PriorRedoPtr != InvalidXLogRecPtr)
		fc_UpdateCheckPointDistanceEstimate(RedoRecPtr - fc_PriorRedoPtr);

	/*
	 * 删除旧的日志文件，这些文件在上一重启点之后不再需要，以防止持有 xlog 的磁盘变满。
	 */
	XLByteToSeg(RedoRecPtr, fc__logSegNo, wal_segment_size);

	/*
	 * 使用当前已重放或接收的 xlog 结束撤回 _logSegNo，
	 * 取其较晚者。
	 */
	fc_receivePtr = GetWalRcvFlushRecPtr(NULL, NULL);
	fc_replayPtr = GetXLogReplayRecPtr(&fc_replayTLI);
	fc_endptr = (fc_receivePtr < fc_replayPtr) ? fc_replayPtr : fc_receivePtr;
	fc_KeepLogSeg(fc_endptr, &fc__logSegNo);
	if (InvalidateObsoleteReplicationSlots(fc__logSegNo))
	{
		/*
		 * 一些插槽已被失效；重新计算旧段的地平线，从RedoRecPtr重新开始。
		 */
		XLByteToSeg(RedoRecPtr, fc__logSegNo, wal_segment_size);
		fc_KeepLogSeg(fc_endptr, &fc__logSegNo);
	}
	fc__logSegNo--;

	/*
	 * 尝试在有用的时间线上回收段。如果自此重启点起我们已被提升，请使用在恢复结束时选择的新时间线。
	 * 如果我们仍在恢复中，请使用当前重放的时间线。
	 *
	 * 对于当前时间线，没有保证 WAL 段会有用；如果恢复在此之后立即进入新时间线，
	 * 那么在这个时间线上预分配的 WAL 段将不会被使用，
	 * 并且会浪费直到下一个重启点的回收。我们将对此忍受。
	 */
	if (!RecoveryInProgress())
		fc_replayTLI = XLogCtl->InsertTimeLineID;

	fc_RemoveOldXlogFiles(fc__logSegNo, RedoRecPtr, fc_endptr, fc_replayTLI);

	/*
	 * 如果需要，生成更多的日志段。（在回收旧的日志段之后执行此操作，因为那可能会提供一些所需的文件。）
	 */
	fc_PreallocXlogFiles(fc_endptr, fc_replayTLI);

	/*
	 * 如果可能，截断 pg_subtrans。我们可以丢弃所有数据，直到任何正在运行的事务的最旧 XMIN。
	 * 未来的事务不会尝试引用任何比这更老的 pg_subtrans 条目（见 subtrans.c 中的 Assert）。
	 * 但是，当热备用被禁用时，我们不能这样做，因为 StartupSUBTRANS 还没有被调用。
	 */
	if (EnableHotStandby)
		TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning());

	/* 实际工作完成；记录并更新统计信息。 */
	fc_LogCheckpointEnd(true);

	/* 重置进程标题 */
	fc_update_checkpoint_display(fc_flags, true, true);

	fc_xtime = GetLatestXTime();
	ereport((log_checkpoints ? LOG : DEBUG2),
			(errmsg("recovery restart point at %X/%X",
					LSN_FORMAT_ARGS(fc_lastCheckPoint.redo)),
			 fc_xtime ? errdetail("Last completed transaction was at log time %s.",
							   timestamptz_to_str(fc_xtime)) : 0));

	/*
	 * 最后，执行 archive_cleanup_command（如有）。
	 */
	if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0)
		ExecuteRecoveryCommand(archiveCleanupCommand,
							   "archive_cleanup_command",
							   false,
							   WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND);

	return true;
}

/*
 * 报告给定目标 LSN 的 WAL 可用性
 * (通常是槽的 restart_lsn)
 *
 * 返回以下枚举值之一：
 *
 * * WALAVAIL_RESERVED 表示 targetLSN 可用，并且在 max_wal_size 范围内。
 *
 * * WALAVAIL_EXTENDED 表示通过保留超出 max_wal_size 的额外段，它仍然可用。如果 max_slot_wal_keep_size 小于 max_wal_size，则不会返回此状态。
 *
 * * WALAVAIL_UNRESERVED 表示它正在丢失，下一个检查点将删除保留的段。使用此槽的 walsender 可能会返回到上述状态。
 *
 * * WALAVAIL_REMOVED 表示它已被移除。具有此 LSN 的槽上的复制流在重启后无法继续。
 *
 * * WALAVAIL_INVALID_LSN 表示槽尚未设置为保留 WAL。
 */
WALAvailability
GetWALAvailability(XLogRecPtr fc_targetLSN)
{
	XLogRecPtr	fc_currpos;		/* 当前写入的 LSN */
	XLogSegNo	fc_currSeg;		/* currpos 的 segid */
	XLogSegNo	fc_targetSeg;		/* targetLSN 的 segid */
	XLogSegNo	fc_oldestSeg;		/* 实际最旧的 segid */
	XLogSegNo	fc_oldestSegMaxWalSize;	/* max_wal_size 保留的最旧 segid */
	XLogSegNo	fc_oldestSlotSeg;	/* 槽保留的最旧 segid */
	uint64		fc_keepSegs;

	/*
	 * 槽不保留 WAL。要么被停用，要么从未激活
	 */
	if (XLogRecPtrIsInvalid(fc_targetLSN))
		return WALAVAIL_INVALID_LSN;

	/*
	 * 计算所有槽当前保留的最旧段，考虑 wal_keep_size 和 max_slot_wal_keep_size。将 oldestSlotSeg 初始化为当前段。
	 */
	fc_currpos = GetXLogWriteRecPtr();
	XLByteToSeg(fc_currpos, fc_oldestSlotSeg, wal_segment_size);
	fc_KeepLogSeg(fc_currpos, &fc_oldestSlotSeg);

	/*
	 * 找到最旧的现存段文件。我们在检查点移除自启动以来的第一个 WAL 段文件之前获得 1，这在某些异常条件下导致状态错误，但实际上不会造成伤害。
	 */
	fc_oldestSeg = XLogGetLastRemovedSegno() + 1;

	/* 通过 max_wal_size 计算最旧段 */
	XLByteToSeg(fc_currpos, fc_currSeg, wal_segment_size);
	fc_keepSegs = ConvertToXSegs(max_wal_size_mb, wal_segment_size) + 1;

	if (fc_currSeg > fc_keepSegs)
		fc_oldestSegMaxWalSize = fc_currSeg - fc_keepSegs;
	else
		fc_oldestSegMaxWalSize = 1;

	/* 我们关心的段 */
	XLByteToSeg(fc_targetLSN, fc_targetSeg, wal_segment_size);

	/*
	 * 如果 targetSeg 已知丢失，则没有必要返回保留或扩展状态值。
	 */
	if (fc_targetSeg >= fc_oldestSlotSeg)
	{
		/* 当 targetSeg 在 max_wal_size 内时显示“reserved” */
		if (fc_targetSeg >= fc_oldestSegMaxWalSize)
			return WALAVAIL_RESERVED;

		/* 通过超出 max_wal_size 的槽保留 */
		return WALAVAIL_EXTENDED;
	}

	/* WAL 段不再被保留，但尚未被移除 */
	if (fc_targetSeg >= fc_oldestSeg)
		return WALAVAIL_UNRESERVED;

	/* 肯定丢失 */
	return WALAVAIL_REMOVED;
}


/*
 * 将 *logSegNo 退回到我们因为 wal_keep_size 或复制槽而需要保留的最后一个段。
 *
 * 这通过从给定的 xlog 位置、recptr 中减去 wal_keep_size 来计算，并确保该结果低于复制槽的要求。对于后者标准，我们确实考虑了 max_slot_wal_keep_size 的影响：从 recptr 保留最多如此多的空间。
 *
 * 关于复制槽的说明：如果此函数计算出的值超出了槽需要保留的值，则受影响的槽需要被失效，并再次调用此函数。
 * XXX 重写此函数可能是个好主意，这样失效可以在这里选择性地进行。
 */
static void fc_KeepLogSeg(XLogRecPtr fc_recptr, XLogSegNo *fc_logSegNo)
{
	XLogSegNo	fc_currSegNo;
	XLogSegNo	fc_segno;
	XLogRecPtr	fc_keep;

	XLByteToSeg(fc_recptr, fc_currSegNo, wal_segment_size);
	fc_segno = fc_currSegNo;

	/*
	 * 首先计算槽保留了多少段，调整 max_slot_wal_keep_size。
	 */
	fc_keep = fc_XLogGetReplicationSlotMinimumLSN();
	if (fc_keep != InvalidXLogRecPtr && fc_keep < fc_recptr)
	{
		XLByteToSeg(fc_keep, fc_segno, wal_segment_size);

		/* 受限于 max_slot_wal_keep_size ... */
		if (max_slot_wal_keep_size_mb >= 0)
		{
			uint64		fc_slot_keep_segs;

			fc_slot_keep_segs =
				ConvertToXSegs(max_slot_wal_keep_size_mb, wal_segment_size);

			if (fc_currSegNo - fc_segno > fc_slot_keep_segs)
				fc_segno = fc_currSegNo - fc_slot_keep_segs;
		}
	}

	/* 但如果设置了，则至少保留 wal_keep_size */
	if (wal_keep_size_mb > 0)
	{
		uint64		fc_keep_segs;

		fc_keep_segs = ConvertToXSegs(wal_keep_size_mb, wal_segment_size);
		if (fc_currSegNo - fc_segno < fc_keep_segs)
		{
			/* 避免下溢，不要低于 1 */
			if (fc_currSegNo <= fc_keep_segs)
				fc_segno = 1;
			else
				fc_segno = fc_currSegNo - fc_keep_segs;
		}
	}

	/* 不要删除比计算出的段更新的 WAL 段 */
	if (fc_segno < *fc_logSegNo)
		*fc_logSegNo = fc_segno;
}

/*
 * 写入一个 NEXTOID 日志记录
 */
void XLogPutNextOid(Oid fc_nextOid)
{
	XLogBeginInsert();
	XLogRegisterData((char *) (&fc_nextOid), sizeof(Oid));
	(void) XLogInsert(RM_XLOG_ID, XLOG_NEXTOID);

	/*
	 * 我们不需要立即刷新 NEXTOID 记录，因为任何刚分配的 OID 只能在插入或更新元组时达到磁盘，该操作会有自己的 XLOG 记录，其必须在 NEXTOID 记录之后。因此，适用于这些记录的标准缓冲区 LSN 互锁将确保没有 OID 在 NEXTOID 记录之前达到磁盘。
	 *
	 * 然而，请注意，上述陈述仅涵盖数据库“内部”的状态。当我们使用生成的 OID 作为文件或目录名时，从某种意义上说，我们违反了基本的 WAL 规则，因为该文件系统的更改可能在 NEXTOID WAL 记录之前到达磁盘。这带来的影响是，如果在此之后发生数据库崩溃，我们可能在重启后重新生成相同的 OID，并发现它与剩余的文件或目录发生冲突。但出于安全起见，我们总是循环直到找到一个不冲突的文件名，这在实践中并不构成真正的问题。请参见 pgsql-hackers 讨论 2006年9月27日。
	 */
}

/*
 * 写入一个 XLOG SWITCH 记录。
 *
 * 在这里，我们只是一味地对该记录发出 XLogInsert 请求。
 * 所有的魔法都发生在 XLogInsert 内部。
 *
 * 返回值为切换记录的结束+1 地址，
 * 或者为之前段的结束+1 地址，如果我们不需要
 * 写入切换记录，因为我们已经在段的开始。
 */
XLogRecPtr RequestXLogSwitch(bool fc_mark_unimportant)
{
	XLogRecPtr	fc_RecPtr;

	/* XLOG SWITCH 没有数据 */
	XLogBeginInsert();

	if (fc_mark_unimportant)
		XLogSetRecordFlags(XLOG_MARK_UNIMPORTANT);
	fc_RecPtr = XLogInsert(RM_XLOG_ID, XLOG_SWITCH);

	return fc_RecPtr;
}

/*
 * 写入一个 RESTORE POINT 记录
 */
XLogRecPtr XLogRestorePoint(const char *fc_rpName)
{
	XLogRecPtr	fc_RecPtr;
	xl_restore_point fc_xlrec;

	fc_xlrec.rp_time = GetCurrentTimestamp();
	strlcpy(fc_xlrec.rp_name, fc_rpName, MAXFNAMELEN);

	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec, sizeof(xl_restore_point));

	fc_RecPtr = XLogInsert(RM_XLOG_ID, XLOG_RESTORE_POINT);

	ereport(LOG,
			(errmsg("restore point \"%s\" created at %X/%X",
					fc_rpName, LSN_FORMAT_ARGS(fc_RecPtr))));

	return fc_RecPtr;
}

/*
 * 检查是否有任何对热备份至关重要的 GUC 参数发生了变化，
 * 如果需要，则在 pg_control 文件中更新该值。
 */
static void fc_XLogReportParameters(void)
{
	if (wal_level != ControlFile->wal_level ||
		wal_log_hints != ControlFile->wal_log_hints ||
		MaxConnections != ControlFile->MaxConnections ||
		max_worker_processes != ControlFile->max_worker_processes ||
		max_wal_senders != ControlFile->max_wal_senders ||
		max_prepared_xacts != ControlFile->max_prepared_xacts ||
		max_locks_per_xact != ControlFile->max_locks_per_xact ||
		track_commit_timestamp != ControlFile->track_commit_timestamp)
	{
		/*
		 * 后台槽位数量的变化不需要 WAL 日志记录，
		 * 如果未启用归档，因为无论如何无法以 wal_level=minimal 开始归档恢复。
		 * 如果 wal_level=minimal，我们实际上也不关心 pg_control 中的值，
		 * 但保持它们的最新状态似乎更好，以避免混淆。
		 */
		if (wal_level != ControlFile->wal_level || XLogIsNeeded())
		{
			xl_parameter_change fc_xlrec;
			XLogRecPtr	fc_recptr;

			fc_xlrec.MaxConnections = MaxConnections;
			fc_xlrec.max_worker_processes = max_worker_processes;
			fc_xlrec.max_wal_senders = max_wal_senders;
			fc_xlrec.max_prepared_xacts = max_prepared_xacts;
			fc_xlrec.max_locks_per_xact = max_locks_per_xact;
			fc_xlrec.wal_level = wal_level;
			fc_xlrec.wal_log_hints = wal_log_hints;
			fc_xlrec.track_commit_timestamp = track_commit_timestamp;

			XLogBeginInsert();
			XLogRegisterData((char *) &fc_xlrec, sizeof(fc_xlrec));

			fc_recptr = XLogInsert(RM_XLOG_ID, XLOG_PARAMETER_CHANGE);
			XLogFlush(fc_recptr);
		}

		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);

		ControlFile->MaxConnections = MaxConnections;
		ControlFile->max_worker_processes = max_worker_processes;
		ControlFile->max_wal_senders = max_wal_senders;
		ControlFile->max_prepared_xacts = max_prepared_xacts;
		ControlFile->max_locks_per_xact = max_locks_per_xact;
		ControlFile->wal_level = wal_level;
		ControlFile->wal_log_hints = wal_log_hints;
		ControlFile->track_commit_timestamp = track_commit_timestamp;
		fc_UpdateControlFile();

		LWLockRelease(ControlFileLock);
	}
}

/*
 * 在共享内存中更新 full_page_writes，如有必要，写入一个
 * XLOG_FPW_CHANGE 记录。
 *
 * 注意：此函数假定没有其他进程同时运行
 * 可能会更新它。
 */
void UpdateFullPageWrites(void)
{
	XLogCtlInsert *fc_Insert = &XLogCtl->Insert;
	bool		fc_recoveryInProgress;

	/*
	 * 如果 full_page_writes 尚未更改，则不执行任何操作。
	 *
	 * 在没有锁的情况下检查共享的 full_page_writes 是安全的，
	 * 因为我们假定没有正在并发运行的进程可以更新它。
	 */
	if (fullPageWrites == fc_Insert->fullPageWrites)
		return;

	/*
	 * 在此关键部分外进行操作，以便由 RecoveryInProgress() 
	 * 完成的 WAL 插入初始化不会触发断言失败。
	 */
	fc_recoveryInProgress = RecoveryInProgress();

	START_CRIT_SECTION();

	/*
	 * 在全页图像方面，即使没有严格要求，始终安全，但反之则不然。因此，如果我们将full_page_writes设置为true，首先将其设置为true，然后写入WAL记录。如果我们将其设置为false，首先写入WAL记录，然后设置全局标志。
	 */
	if (fullPageWrites)
	{
		fc_WALInsertLockAcquireExclusive();
		fc_Insert->fullPageWrites = true;
		fc_WALInsertLockRelease();
	}

	/*
	 * 写入一个XLOG_FPW_CHANGE记录。这使我们能够在归档恢复期间跟踪full_page_writes（如果需要的话）。
	 */
	if (XLogStandbyInfoActive() && !fc_recoveryInProgress)
	{
		XLogBeginInsert();
		XLogRegisterData((char *) (&fullPageWrites), sizeof(bool));

		XLogInsert(RM_XLOG_ID, XLOG_FPW_CHANGE);
	}

	if (!fullPageWrites)
	{
		fc_WALInsertLockAcquireExclusive();
		fc_Insert->fullPageWrites = false;
		fc_WALInsertLockRelease();
	}
	END_CRIT_SECTION();
}

/*
 * XLOG资源管理器的例程
 *
 * info值的定义在include/catalog/pg_control.h中，尽管并非所有记录类型都与控制文件更新相关。
 *
 * 注意：一些直接与WAL恢复相关的XLOG记录类型在xlogrecovery_redo()中处理。
 */
void xlog_redo(XLogReaderState *fc_record)
{
	uint8		fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;

	/*
	 * 在XLOG rmgr中，备份块仅用于XLOG_FPI和XLOG_FPI_FOR_HINT记录。
	 */
	Assert(fc_info == XLOG_FPI || fc_info == XLOG_FPI_FOR_HINT ||
		   !XLogRecHasAnyBlockRefs(fc_record));

	if (fc_info == XLOG_NEXTOID)
	{
		Oid			fc_nextOid;

		/*
		 * 我们以前试图取ShmemVariableCache->nextOid和记录的nextOid的最大值，但如果OID计数器环绕，这会失败。由于在重放期间不应发生OID分配，因此最好完全相信记录。我们在设置变量时仍然获取OidGenLock，以防万一。
		 */
		memcpy(&fc_nextOid, XLogRecGetData(fc_record), sizeof(Oid));
		LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
		ShmemVariableCache->nextOid = fc_nextOid;
		ShmemVariableCache->oidCount = 0;
		LWLockRelease(OidGenLock);
	}
	else if (fc_info == XLOG_CHECKPOINT_SHUTDOWN)
	{
		CheckPoint	fc_checkPoint;
		TimeLineID	fc_replayTLI;

		memcpy(&fc_checkPoint, XLogRecGetData(fc_record), sizeof(CheckPoint));
		/* 在SHUTDOWN检查点中，准确地相信计数器 */
		LWLockAcquire(XidGenLock, LW_EXCLUSIVE);
		ShmemVariableCache->nextXid = fc_checkPoint.nextXid;
		LWLockRelease(XidGenLock);
		LWLockAcquire(OidGenLock, LW_EXCLUSIVE);
		ShmemVariableCache->nextOid = fc_checkPoint.nextOid;
		ShmemVariableCache->oidCount = 0;
		LWLockRelease(OidGenLock);
		MultiXactSetNextMXact(fc_checkPoint.nextMulti,
							  fc_checkPoint.nextMultiOffset);

		MultiXactAdvanceOldest(fc_checkPoint.oldestMulti,
							   fc_checkPoint.oldestMultiDB);

		/*
		 * 这里不需要设置oldestClogXid；如果自初始化以来已更改，将在我们重做xl_clog_truncate时进行设置。
		 */
		SetTransactionIdLimit(fc_checkPoint.oldestXid, fc_checkPoint.oldestXidDB);

		/*
		 * 如果在等待备份结束记录时看到一个关闭检查点，则备份已被取消，备份结束记录将永远不会到达。
		 */
		if (ArchiveRecoveryRequested &&
			!XLogRecPtrIsInvalid(ControlFile->backupStartPoint) &&
			XLogRecPtrIsInvalid(ControlFile->backupEndPoint))
			ereport(PANIC,
					(errmsg("online backup was canceled, recovery cannot continue")));

		/*
		 * 如果我们看到一个关闭检查点，我们知道此时在主服务器上没有任何正在运行的内容。因此虚假生成一个空的running-xacts记录并在此时使用它。恢复准备好事务的额外备份状态。
		 */
		if (standbyState >= STANDBY_INITIALIZED)
		{
			TransactionId *fc_xids;
			int			fc_nxids;
			TransactionId fc_oldestActiveXID;
			TransactionId fc_latestCompletedXid;
			RunningTransactionsData fc_running;

			fc_oldestActiveXID = PrescanPreparedTransactions(&fc_xids, &fc_nxids);

			/* 更新任何准备事务的pg_subtrans条目 */
			StandbyRecoverPreparedTransactions();

			/*
			 * 构造一个表示关闭服务器的RunningTransactions快照，只有准备好的事务仍然活着。此时我们永远不会溢出，因为所有的subxids都与其父准备事务一起列出。
			 */
			fc_running.xcnt = fc_nxids;
			fc_running.subxcnt = 0;
			fc_running.subxid_status = SUBXIDS_IN_SUBTRANS;
			fc_running.nextXid = XidFromFullTransactionId(fc_checkPoint.nextXid);
			fc_running.oldestRunningXid = fc_oldestActiveXID;
			fc_latestCompletedXid = XidFromFullTransactionId(fc_checkPoint.nextXid);
			TransactionIdRetreat(fc_latestCompletedXid);
			Assert(TransactionIdIsNormal(fc_latestCompletedXid));
			fc_running.latestCompletedXid = fc_latestCompletedXid;
			fc_running.xids = fc_xids;

			ProcArrayApplyRecoveryInfo(&fc_running);
		}

		/* ControlFile->checkPointCopy始终跟踪最新的ckpt XID */
		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
		ControlFile->checkPointCopy.nextXid = fc_checkPoint.nextXid;
		LWLockRelease(ControlFileLock);

		/* 更新检查点XID/epoch的共享内存副本 */
		SpinLockAcquire(&XLogCtl->info_lck);
		XLogCtl->ckptFullXid = fc_checkPoint.nextXid;
		SpinLockRelease(&XLogCtl->info_lck);

		/*
		 * 我们应该在重放此记录之前已经切换到新的TLI。
		 */
		(void) GetCurrentReplayRecPtr(&fc_replayTLI);
		if (fc_checkPoint.ThisTimeLineID != fc_replayTLI)
			ereport(PANIC,
					(errmsg("unexpected timeline ID %u (should be %u) in shutdown checkpoint record",
							fc_checkPoint.ThisTimeLineID, fc_replayTLI)));

		fc_RecoveryRestartPoint(&fc_checkPoint, fc_record);
	}
	else if (fc_info == XLOG_CHECKPOINT_ONLINE)
	{
		CheckPoint	fc_checkPoint;
		TimeLineID	fc_replayTLI;

		memcpy(&fc_checkPoint, XLogRecGetData(fc_record), sizeof(CheckPoint));
		/* 在ONLINE检查点中，将XID计数器视为最小值 */
		LWLockAcquire(XidGenLock, LW_EXCLUSIVE);
		if (FullTransactionIdPrecedes(ShmemVariableCache->nextXid,
									  fc_checkPoint.nextXid))
			ShmemVariableCache->nextXid = fc_checkPoint.nextXid;
		LWLockRelease(XidGenLock);

		/*
		 * 我们在在线检查点中忽略 nextOid 计数器，倾向于通过 XLOG_NEXTOID 记录来跟踪 OID 分配。nextOid 
		 * 计数器来自检查点开始，可能与后来的 XLOG_NEXTOID 记录相比是过时的。我们可以尝试取 nextOid 
		 * 计数器和我们最新值的最大值，但由于没有特别保证 OID 
		 * 计数器环绕的速度，这是一件风险很大的事情。无论如何，nextOid 
		 * 计数器的用户都必须避免重复分配，因此稍微过时的值应该是安全的。
		 */

		/* 处理 multixact */
		MultiXactAdvanceNextMXact(fc_checkPoint.nextMulti,
								  fc_checkPoint.nextMultiOffset);

		/*
		 * 注意：在重放由旧主生成的 WAL 时，这可能会执行 multixact 截断。
		 */
		MultiXactAdvanceOldest(fc_checkPoint.oldestMulti,
							   fc_checkPoint.oldestMultiDB);
		if (TransactionIdPrecedes(ShmemVariableCache->oldestXid,
								  fc_checkPoint.oldestXid))
			SetTransactionIdLimit(fc_checkPoint.oldestXid,
								  fc_checkPoint.oldestXidDB);
		/* ControlFile->checkPointCopy始终跟踪最新的ckpt XID */
		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
		ControlFile->checkPointCopy.nextXid = fc_checkPoint.nextXid;
		LWLockRelease(ControlFileLock);

		/* 更新检查点XID/epoch的共享内存副本 */
		SpinLockAcquire(&XLogCtl->info_lck);
		XLogCtl->ckptFullXid = fc_checkPoint.nextXid;
		SpinLockRelease(&XLogCtl->info_lck);

		/* TLI 在在线检查点中不应更改 */
		(void) GetCurrentReplayRecPtr(&fc_replayTLI);
		if (fc_checkPoint.ThisTimeLineID != fc_replayTLI)
			ereport(PANIC,
					(errmsg("unexpected timeline ID %u (should be %u) in online checkpoint record",
							fc_checkPoint.ThisTimeLineID, fc_replayTLI)));

		fc_RecoveryRestartPoint(&fc_checkPoint, fc_record);
	}
	else if (fc_info == XLOG_OVERWRITE_CONTRECORD)
	{
		/* 这里没有要做的，已在 xlogrecovery_redo() 中处理 */
	}
	else if (fc_info == XLOG_END_OF_RECOVERY)
	{
		xl_end_of_recovery fc_xlrec;
		TimeLineID	fc_replayTLI;

		memcpy(&fc_xlrec, XLogRecGetData(fc_record), sizeof(xl_end_of_recovery));

		/*
		 * 对于热备份，我们可以将其视为关闭检查点，但这种情况更少且更难测试，因此其带来的好处并不超过
		 * 潜在的额外维护成本。
		 */

		/*
		 * 我们应该在重放此记录之前已经切换到新的TLI。
		 */
		(void) GetCurrentReplayRecPtr(&fc_replayTLI);
		if (fc_xlrec.ThisTimeLineID != fc_replayTLI)
			ereport(PANIC,
					(errmsg("unexpected timeline ID %u (should be %u) in end-of-recovery record",
							fc_xlrec.ThisTimeLineID, fc_replayTLI)));
	}
	else if (fc_info == XLOG_NOOP)
	{
		/* 此处无操作 */
	}
	else if (fc_info == XLOG_SWITCH)
	{
		/* 此处无操作 */
	}
	else if (fc_info == XLOG_RESTORE_POINT)
	{
		/* 这里没有要做的，已在 xlogrecovery.c 中处理 */
	}
	else if (fc_info == XLOG_FPI || fc_info == XLOG_FPI_FOR_HINT)
	{
		/*
		 * XLOG_FPI 记录仅包含一个或多个块引用。每个块引用必须包含一个完整页面图像，即使在生成记录时
		 * full_page_writes 被禁用 - 否则该记录将没有意义。
		 *
		 * XLOG_FPI_FOR_HINT 记录在页面需要因提示位更新而被 WAL 日志记录时生成。仅在启用校验和和/或 
		 * wal_log_hints 时生成它们。如果在生成时 disabled full_page_writes，则可能不包含完整的页面图像。
		 * 在这种情况下，这里没有要做的。
		 *
		 * 这些通用记录不会生成恢复冲突 - 如果资源管理器需要生成冲突，必须定义一个单独的 WAL 
		 * 记录类型和重做例程。
		 */
		for (uint8 fc_block_id = 0; fc_block_id <= XLogRecMaxBlockId(fc_record); fc_block_id++)
		{
			Buffer		fc_buffer;

			if (!XLogRecHasBlockImage(fc_record, fc_block_id))
			{
				if (fc_info == XLOG_FPI)
					elog(ERROR, "XLOG_FPI record did not contain a full-page image");
				continue;
			}

			if (XLogReadBufferForRedo(fc_record, fc_block_id, &fc_buffer) != BLK_RESTORED)
				elog(ERROR, "unexpected XLogReadBufferForRedo result when restoring backup block");
			UnlockReleaseBuffer(fc_buffer);
		}
	}
	else if (fc_info == XLOG_BACKUP_END)
	{
		/* 这里没有要做的，已在 xlogrecovery_redo() 中处理 */
	}
	else if (fc_info == XLOG_PARAMETER_CHANGE)
	{
		xl_parameter_change fc_xlrec;

		/* 更新我们在 pg_control 中的参数副本 */
		memcpy(&fc_xlrec, XLogRecGetData(fc_record), sizeof(xl_parameter_change));

		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
		ControlFile->MaxConnections = fc_xlrec.MaxConnections;
		ControlFile->max_worker_processes = fc_xlrec.max_worker_processes;
		ControlFile->max_wal_senders = fc_xlrec.max_wal_senders;
		ControlFile->max_prepared_xacts = fc_xlrec.max_prepared_xacts;
		ControlFile->max_locks_per_xact = fc_xlrec.max_locks_per_xact;
		ControlFile->wal_level = fc_xlrec.wal_level;
		ControlFile->wal_log_hints = fc_xlrec.wal_log_hints;

		/*
		 * 更新 minRecoveryPoint，以确保如果恢复中止，我们在允许热备份之前恢复到此点。
		 * 如果 max_* 设置降低，这一点很重要，以确保您不对更改之前的 WAL 运行查询。只要崩溃恢复
		 * 正在进行，局部副本就无法更新，我们期望所有的 WAL 都被重放。
		 */
		if (InArchiveRecovery)
		{
			LocalMinRecoveryPoint = ControlFile->minRecoveryPoint;
			LocalMinRecoveryPointTLI = ControlFile->minRecoveryPointTLI;
		}
		if (LocalMinRecoveryPoint != InvalidXLogRecPtr && LocalMinRecoveryPoint < fc_lsn)
		{
			TimeLineID	fc_replayTLI;

			(void) GetCurrentReplayRecPtr(&fc_replayTLI);
			ControlFile->minRecoveryPoint = fc_lsn;
			ControlFile->minRecoveryPointTLI = fc_replayTLI;
		}

		CommitTsParameterChange(fc_xlrec.track_commit_timestamp,
								ControlFile->track_commit_timestamp);
		ControlFile->track_commit_timestamp = fc_xlrec.track_commit_timestamp;

		fc_UpdateControlFile();
		LWLockRelease(ControlFileLock);

		/* 检查任何参数更改是否在恢复时导致问题 */
		fc_CheckRequiredParameterValues();
	}
	else if (fc_info == XLOG_FPW_CHANGE)
	{
		bool		fc_fpw;

		memcpy(&fc_fpw, XLogRecGetData(fc_record), sizeof(bool));

		/*
		 * 更新最后一次重播的 XLOG_FPW_CHANGE 记录的 LSN，以便
		 * do_pg_backup_start() 和 do_pg_backup_stop() 可以检查
		 * 在在线备份期间是否禁用了 full_page_writes。
		 */
		if (!fc_fpw)
		{
			SpinLockAcquire(&XLogCtl->info_lck);
			if (XLogCtl->lastFpwDisableRecPtr < fc_record->ReadRecPtr)
				XLogCtl->lastFpwDisableRecPtr = fc_record->ReadRecPtr;
			SpinLockRelease(&XLogCtl->info_lck);
		}

		/* 跟踪 full_page_writes */
		lastFullPageWrites = fc_fpw;
	}
}

/*
 * 根据 GUC wal_sync_method 的值返回用于打开文件的（可能的）同步标志。
 */
static int fc_get_sync_bit(int fc_method)
{
	int			fc_o_direct_flag = 0;

	/* 如果 fsync 被禁用，则绝不要以同步模式打开 */
	if (!enableFsync)
		return 0;

	/*
	 * 通过在使用 O_SYNC/O_FSYNC 和 O_DSYNC 时绕过内核缓存来优化写入。
	 * 但仅当归档和流式传输被禁用时，否则归档命令或 walsender 进程将在
	 * 写入 WAL 后不久读取它，如果我们绕过了内核缓存，这将保证导致物理
	 * 读取。我们还因为同样的原因跳过在 XLogFileClose() 中的
	 * posix_fadvise(POSIX_FADV_DONTNEED) 调用。
	 *
	 * 由于类似的原因，walreceiver 进程中永远不要使用 O_DIRECT；walreceiver
	 * 写入的 WAL 通常会在写入后不久被启动进程读取。此外，walreceiver 执行
	 * 未对齐的写入，这与 O_DIRECT 不兼容，因此这也是正确性所必需的。
	 */
	if (!XLogIsNeeded() && !AmWalReceiverProcess())
		fc_o_direct_flag = PG_O_DIRECT;

	switch (fc_method)
	{
			/*
			 * 所有同步选项的枚举值都被定义，即使它们在当前平台上不被支持。
			 * 但如果不支持，它们不会被包含在枚举选项数组中，因此在这里
			 * 不会看到。
			 */
		case SYNC_METHOD_FSYNC:
		case SYNC_METHOD_FSYNC_WRITETHROUGH:
		case SYNC_METHOD_FDATASYNC:
			return 0;
#ifdef OPEN_SYNC_FLAG
		case SYNC_METHOD_OPEN:
			return OPEN_SYNC_FLAG | fc_o_direct_flag;
#endif
#ifdef OPEN_DATASYNC_FLAG
		case SYNC_METHOD_OPEN_DSYNC:
			return OPEN_DATASYNC_FLAG | fc_o_direct_flag;
#endif
		default:
			/* 不可能发生（除非我们与选项数组不同步） */
			elog(ERROR, "unrecognized wal_sync_method: %d", fc_method);
			return 0;			/* 消除警告 */
	}
}

/*
 * GUC 支持
 */
void assign_xlog_sync_method(int fc_new_sync_method, void *fc_extra)
{
	if (sync_method != fc_new_sync_method)
	{
		/*
		 * 为了确保没有块未同步，强制对当前打开的日志段（如果存在）进行
		 * fsync。如果打开标志发生变化，则关闭日志文件，以便在下次使用时
		 * 以新标志位重新打开。
		 */
		if (openLogFile >= 0)
		{
			pgstat_report_wait_start(WAIT_EVENT_WAL_SYNC_METHOD_ASSIGN);
			if (pg_fsync(openLogFile) != 0)
			{
				char		fc_xlogfname[MAXFNAMELEN];
				int			fc_save_errno;

				fc_save_errno = errno;
				XLogFileName(fc_xlogfname, openLogTLI, openLogSegNo,
							 wal_segment_size);
				errno = fc_save_errno;
				ereport(PANIC,
						(errcode_for_file_access(),
						 errmsg("could not fsync file \"%s\": %m", fc_xlogfname)));
			}

			pgstat_report_wait_end();
			if (fc_get_sync_bit(sync_method) != fc_get_sync_bit(fc_new_sync_method))
				fc_XLogFileClose();
		}
	}
}


/*
 * 对 XLOG 输出文件发出适当类型的 fsync（如有）。
 *
 * 'fd' 是要 fsync 的 XLOG 文件的文件描述符。
 * 'segno' 用于错误报告目的。
 */
void issue_xlog_fsync(int fc_fd, XLogSegNo fc_segno, TimeLineID fc_tli)
{
	char	   *fc_msg = NULL;
	instr_time	fc_start;

	Assert(fc_tli != 0);

	/*
	 * 如果 fsync 被禁用或 write() 已经同步 WAL 文件，则快速退出。
	 */
	if (!enableFsync ||
		sync_method == SYNC_METHOD_OPEN ||
		sync_method == SYNC_METHOD_OPEN_DSYNC)
		return;

	/* 测量同步 WAL 文件的 I/O 时间 */
	if (track_wal_io_timing)
		INSTR_TIME_SET_CURRENT(fc_start);

	pgstat_report_wait_start(WAIT_EVENT_WAL_SYNC);
	switch (sync_method)
	{
		case SYNC_METHOD_FSYNC:
			if (pg_fsync_no_writethrough(fc_fd) != 0)
				fc_msg = _("could not fsync file \"%s\": %m");
			break;
#ifdef HAVE_FSYNC_WRITETHROUGH
		case SYNC_METHOD_FSYNC_WRITETHROUGH:
			if (pg_fsync_writethrough(fc_fd) != 0)
				fc_msg = _("could not fsync write-through file \"%s\": %m");
			break;
#endif
#ifdef HAVE_FDATASYNC
		case SYNC_METHOD_FDATASYNC:
			if (pg_fdatasync(fc_fd) != 0)
				fc_msg = _("could not fdatasync file \"%s\": %m");
			break;
#endif
		case SYNC_METHOD_OPEN:
		case SYNC_METHOD_OPEN_DSYNC:
			/* 不可达 */
			Assert(false);
			break;
		default:
			elog(PANIC, "unrecognized wal_sync_method: %d", sync_method);
			break;
	}

	/* 如果 fsync 失败则 PANIC */
	if (fc_msg)
	{
		char		fc_xlogfname[MAXFNAMELEN];
		int			fc_save_errno = errno;

		XLogFileName(fc_xlogfname, fc_tli, fc_segno, wal_segment_size);
		errno = fc_save_errno;
		ereport(PANIC,
				(errcode_for_file_access(),
				 errmsg(fc_msg, fc_xlogfname)));
	}

	pgstat_report_wait_end();

	/*
	 * 增加 I/O 时间和 WAL 文件同步次数。
	 */
	if (track_wal_io_timing)
	{
		instr_time	fc_duration;

		INSTR_TIME_SET_CURRENT(fc_duration);
		INSTR_TIME_SUBTRACT(fc_duration, fc_start);
		PendingWalStats.wal_sync_time += INSTR_TIME_GET_MICROSEC(fc_duration);
	}

	PendingWalStats.wal_sync++;
}

/*
 * do_pg_backup_start 是用户可见的 pg_backup_start() 函数的主力程序。它创建必要的起始检查点，并构造备份标签和表空间映射。
 *
 * 输入参数为 "backupidstr"（备份标签字符串）和 "fast"（如果为 true，我们以立即模式执行检查点以加快速度）。
 *
 * 备份标签和表空间映射的内容被附加到 *labelfile 和 *tblspcmapfile，调用者负责将它们包含在备份归档中作为 'backup_label' 和 'tablespace_map'。
 * tblspcmapfile 主要是为了在 Windows 中的 tar 格式，因为原生 Windows 工具在从 tar 中提取文件时无法创建符号链接。
 * 然而，为了一致性和平台独立性，我们在所有地方都以相同的方式进行处理。
 *
 * 如果 "tablespaces" 不是 NULL，它将接收描述集群表空间的 tablespaceinfo 结构列表。
 *
 * 返回必须存在于此备份中以恢复的最小 WAL 位置，以及在 *starttli_p 中的相应时间线 ID。
 *
 * 每个成功启动的备份必须通过调用 do_pg_backup_stop() 或 do_pg_abort_backup() 进行停止。可以同时有多个备份处于活动状态。
 *
 * 调用此函数的用户必须验证调用用户的权限！
 */
XLogRecPtr do_pg_backup_start(const char *fc_backupidstr, bool fc_fast, TimeLineID *fc_starttli_p,
				   StringInfo fc_labelfile, List **fc_tablespaces,
				   StringInfo fc_tblspcmapfile)
{
	bool		fc_backup_started_in_recovery = false;
	XLogRecPtr	fc_checkpointloc;
	XLogRecPtr	fc_startpoint;
#ifdef FDD
	XLogRecPtr	fc_slot_startpoint = InvalidXLogRecPtr;
#endif
	TimeLineID	fc_starttli;
	pg_time_t	fc_stamp_time;
	char		fc_strfbuf[128];
	char		fc_xlogfilename[MAXFNAMELEN];
	XLogSegNo	fc__logSegNo;

	fc_backup_started_in_recovery = RecoveryInProgress();

	/*
	 * 在恢复过程中，我们不需要检查 WAL 级别。因为，如果 WAL 级别不足，在恢复期间不可能到达这里。
	 */
	if (!fc_backup_started_in_recovery && !XLogIsNeeded())
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("WAL level not sufficient for making an online backup"),
				 errhint("wal_level must be set to \"replica\" or \"logical\" at server start.")));

	if (strlen(fc_backupidstr) > MAXPGPATH)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("backup label too long (max %d bytes)",
						MAXPGPATH)));

	/*
	 * 在共享内存中标记备份处于活动状态。我们必须在在线备份期间执行全页 WAL 写入，即使在其他时间不这样做，因为备份转储可能会在我们写入同一页面时并行读取页面，从而获取到 "撕裂"（部分写入） 的数据库页面副本。
	 * 只要 WAL 序列中对页面的第一次写入是全页写入，这可以得到修复。因此，我们开启 forcePageWrites，然后强制执行 CHECKPOINT，以确保共享内存中没有脏页在备份进行时被转储，而没有对应的 WAL 记录。
	 * （备份完成后，我们不再需要强制全页写入，因为我们预期在备份期间未修改的任何页面都必须已经被正确捕获。）
	 *
	 * 请注意，从备用服务器进行在线备份时，forcePageWrites 不会产生影响。
	 *
	 * 我们必须持有所有插入锁才能更改 forcePageWrites 的值，以确保对 XLogInsertRecord() 进行充分的互锁。
	 */
	fc_WALInsertLockAcquireExclusive();
	XLogCtl->Insert.runningBackups++;
	XLogCtl->Insert.forcePageWrites = true;
	fc_WALInsertLockRelease();

	/* 如果下面的操作失败，确保释放 forcePageWrites */
	PG_ENSURE_ERROR_CLEANUP(fc_pg_backup_start_callback, (Datum) 0);
	{
		bool		fc_gotUniqueStartpoint = false;
		DIR		   *fc_tblspcdir;
		struct dirent *fc_de;
		tablespaceinfo *fc_ti;
		int			fc_datadirpathlen;

		/*
		 * 在检查点之前强制执行 XLOG 文件切换，以确保检查点
		 * 写入的 WAL 段不包含旧时间线 ID 的页面。 如果在从 PITR
		 * 存档恢复后立即调用 pg_backup_start()，则会发生这种情况：
		 * 第一 WAL 段包含启动检查点的页面在开头具有旧时间线 ID。
		 * 这可能会在恢复时造成麻烦：如果 pg_wal 目录未包含
		 * 在基础备份中，并且在开始备份之前已清除 WAL 存档，
		 * 我们将没有覆盖旧时间线的历史文件。
		 *
		 * 这还确保我们已经发出了具有 XLP_BKP_REMOVABLE 关闭的
		 * WAL 页面头，在发出检查点记录之前。 因此，如果 WAL
		 * 存档器（例如 pglesslog）试图压缩可移除备份块，
		 * 它不会删除在此点之后发生的任何块。
		 *
		 * 在恢复期间，我们跳过强制执行 XLOG 文件切换，这意味着
		 * 在恢复期间进行的备份不可用于上述特殊恢复案例。
		 */
		if (!fc_backup_started_in_recovery)
			RequestXLogSwitch(false);

		do
		{
			bool		fc_checkpointfpw;

			/*
			 * 强制执行检查点。 除了防止撕裂页面问题外，这还确保
			 * 两次连续的备份运行将具有不同的检查点位置，因此
			 * 即使在两次之间没有发生任何事情，历史文件名也会
			 * 不同。
			 *
			 * 在恢复期间，如果可能，建立重启点。 我们使用
			 * 最后一个重启点作为备份起始检查点。 这意味着
			 * 两次连续的备份运行可以具有相同的检查点位置。
			 *
			 * 由于我们在恢复期间执行 do_pg_backup_start()，
			 * 这意味着检查点正在运行，我们可以使用
			 * RequestCheckpoint() 来建立重启点。
			 *
			 * 我们仅在用户请求时使用 CHECKPOINT_IMMEDIATE
			 * （通过传递 fast = true）。否则，这可能会需要一些时间。
			 */
			RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT |
							  (fc_fast ? CHECKPOINT_IMMEDIATE : 0));

			/*
			 * 现在我们需要获取检查点记录位置，以及
			 * 其 REDO 指针。 从检查点恢复所需的 WAL 最早点
			 * 正是 REDO 指针。
			 */
			LWLockAcquire(ControlFileLock, LW_SHARED);
			fc_checkpointloc = ControlFile->checkPoint;
			fc_startpoint = ControlFile->checkPointCopy.redo;
			fc_starttli = ControlFile->checkPointCopy.ThisTimeLineID;
			fc_checkpointfpw = ControlFile->checkPointCopy.fullPageWrites;
			LWLockRelease(ControlFileLock);

#ifdef FDD
			/*
			 * 如果正在使用故障转移槽，我们必须保留并转移
			 * 比重做位置更早的 WAL，以便在故障转移事件后
			 * 能够重放这些槽。
			 *
			 * 这必须在 xlog 段边界，因此需要适当地截断 LSN。
			 */
			if (max_replication_slots > 0)
			{
				fc_slot_startpoint = (GetReplicationSlotsComputeRequiredLSN(true)/ wal_segment_size) * wal_segment_size;
			}
#endif

			if (fc_backup_started_in_recovery)
			{
				XLogRecPtr	fc_recptr;

				/*
				 * 检查在在线备份期间重播的所有 WAL
				 * （即，自上次作为备份起始检查点使用的重启点起）
				 * 是否包含完整页面写入。
				 */
				SpinLockAcquire(&XLogCtl->info_lck);
				fc_recptr = XLogCtl->lastFpwDisableRecPtr;
				SpinLockRelease(&XLogCtl->info_lck);

				if (!fc_checkpointfpw || fc_startpoint <= fc_recptr)
					ereport(ERROR,
							(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
							 errmsg("WAL generated with full_page_writes=off was replayed "
									"since last restartpoint"),
							 errhint("This means that the backup being taken on the standby "
									 "is corrupt and should not be used. "
									 "Enable full_page_writes and run CHECKPOINT on the primary, "
									 "and then try an online backup again.")));

				/*
				 * 在恢复期间，由于我们不使用结束备份的 WAL
				 * 记录并且不写入备份历史文件，起始 WAL 位置
				 * 不需要是唯一的。 这意味着同时开始的两个基础
				 * 备份可能会使用相同的检查点作为起始位置。
				 */
				fc_gotUniqueStartpoint = true;
			}

			/*
			 * 如果两个基础备份同时启动（在WAL发送进程中），我们需要确保它们使用不同的检查点作为起始位置，因为我们使用起始WAL位置作为基础备份在备份结束WAL记录和写入备份历史文件时的唯一标识符。也许为每个备份生成一个单独的唯一ID会更好，而不是强制另一个检查点，但在另一个检查点之后立即进行检查点也并不昂贵，因为只有少量缓冲区还未被修改。
			 */
			fc_WALInsertLockAcquireExclusive();
			if (XLogCtl->Insert.lastBackupStart < fc_startpoint)
			{
				XLogCtl->Insert.lastBackupStart = fc_startpoint;
				fc_gotUniqueStartpoint = true;
			}
			fc_WALInsertLockRelease();
		} while (!fc_gotUniqueStartpoint);

		XLByteToSeg(fc_startpoint, fc__logSegNo, wal_segment_size);
		XLogFileName(fc_xlogfilename, fc_starttli, fc__logSegNo, wal_segment_size);

		/*
		 * 构造tablespace_map文件。
		 */
		fc_datadirpathlen = strlen(DataDir);

		/* 收集有关所有表空间的信息 */
		fc_tblspcdir = AllocateDir("pg_tblspc");
		while ((fc_de = ReadDir(fc_tblspcdir, "pg_tblspc")) != NULL)
		{
			char		fc_fullpath[MAXPGPATH + 10];
			char		fc_linkpath[MAXPGPATH];
			char	   *fc_relpath = NULL;
			int			fc_rllen;
			StringInfoData fc_escapedpath;
			char	   *fc_s;

			/* 跳过任何看起来不像表空间的内容 */
			if (strspn(fc_de->d_name, "0123456789") != strlen(fc_de->d_name))
				continue;

			snprintf(fc_fullpath, sizeof(fc_fullpath), "pg_tblspc/%s", fc_de->d_name);

			/*
			 * 跳过任何不是符号链接/junction的内容。仅供测试，
			 * 我们有时使用allow_in_place_tablespaces直接在pg_tblspc下创建
			 * 目录，这在下面会失败。
			 */
			if (get_dirent_type(fc_fullpath, fc_de, false, ERROR) != PGFILETYPE_LNK)
				continue;

#if defined(HAVE_READLINK) || defined(WIN32)
			fc_rllen = readlink(fc_fullpath, fc_linkpath, sizeof(fc_linkpath));
			if (fc_rllen < 0)
			{
				ereport(WARNING,
						(errmsg("could not read symbolic link \"%s\": %m",
								fc_fullpath)));
				continue;
			}
			else if (fc_rllen >= sizeof(fc_linkpath))
			{
				ereport(WARNING,
						(errmsg("symbolic link \"%s\" target is too long",
								fc_fullpath)));
				continue;
			}
			fc_linkpath[fc_rllen] = '\0';

			/*
			 * 构建一个带反斜杠转义的链接路径版本以包含
			 * 在表空间映射文件中。
			 */
			initStringInfo(&fc_escapedpath);
			for (fc_s = fc_linkpath; *fc_s; fc_s++)
			{
				if (*fc_s == '\n' || *fc_s == '\r' || *fc_s == '\\')
					appendStringInfoChar(&fc_escapedpath, '\\');
				appendStringInfoChar(&fc_escapedpath, *fc_s);
			}

			/*
			 * Relpath保存表空间目录的相对路径，
			 * 当它位于PGDATA内时，或者如果它位于其他地方则为NULL。
			 */
			if (fc_rllen > fc_datadirpathlen &&
				strncmp(fc_linkpath, DataDir, fc_datadirpathlen) == 0 &&
				IS_DIR_SEP(fc_linkpath[fc_datadirpathlen]))
				fc_relpath = fc_linkpath + fc_datadirpathlen + 1;

			fc_ti = palloc(sizeof(tablespaceinfo));
			fc_ti->oid = pstrdup(fc_de->d_name);
			fc_ti->path = pstrdup(fc_linkpath);
			fc_ti->rpath = fc_relpath ? pstrdup(fc_relpath) : NULL;
			fc_ti->size = -1;

			if (fc_tablespaces)
				*fc_tablespaces = lappend(*fc_tablespaces, fc_ti);

			appendStringInfo(fc_tblspcmapfile, "%s %s\n",
							 fc_ti->oid, fc_escapedpath.data);

			pfree(fc_escapedpath.data);
#else

			/*
			 * 如果平台没有符号链接，就不可能存在表空间 -
			 * 显然是其他人创建了它们。对此发出警告并忽略。
			 */
			ereport(WARNING,
					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
					 errmsg("tablespaces are not supported on this platform")));
#endif
		}
		FreeDir(fc_tblspcdir);

		/*
		 * 构造备份标签文件。
		 */

		/* 在这里使用日志时区，而不是会话时区 */
		fc_stamp_time = (pg_time_t) time(NULL);
		pg_strftime(fc_strfbuf, sizeof(fc_strfbuf),
					"%Y-%m-%d %H:%M:%S %Z",
					pg_localtime(&fc_stamp_time, log_timezone));
		appendStringInfo(fc_labelfile, "START WAL LOCATION: %X/%X (file %s)\n",
						 LSN_FORMAT_ARGS(fc_startpoint), fc_xlogfilename);
		appendStringInfo(fc_labelfile, "CHECKPOINT LOCATION: %X/%X\n",
						 LSN_FORMAT_ARGS(fc_checkpointloc));
		appendStringInfo(fc_labelfile, "BACKUP METHOD: streamed\n");
		appendStringInfo(fc_labelfile, "BACKUP FROM: %s\n",
						 fc_backup_started_in_recovery ? "standby" : "primary");
		appendStringInfo(fc_labelfile, "START TIME: %s\n", fc_strfbuf);
		appendStringInfo(fc_labelfile, "LABEL: %s\n", fc_backupidstr);
		appendStringInfo(fc_labelfile, "START TIMELINE: %u\n", fc_starttli);
#ifdef FDD
		if (fc_slot_startpoint != InvalidXLogRecPtr)
					appendStringInfo(fc_labelfile,  "MIN FAILOVER SLOT LSN: %X/%X\n",
								LSN_FORMAT_ARGS(fc_slot_startpoint));
#endif
	}
	PG_END_ENSURE_ERROR_CLEANUP(fc_pg_backup_start_callback, (Datum) 0);

	/*
	 * 标记备份的开始阶段已正确完成。
	 */
	sessionBackupState = SESSION_BACKUP_RUNNING;

#ifdef FDD
	/*	 
	 * pg_basebackup等期望使用此位置开始复制
	 * WAL，因此我们应返回插槽起始LSN与当前重做位置的最小值，以确保获取故障切换
	 * 插槽所需的所有WAL。
	 *
	 * 故障切换插槽所需的最小LSN也可以从备份标签文件中的
	 * 'MIN FAILOVER SLOT LSN'项中获取。
	 */
	if (fc_slot_startpoint != InvalidXLogRecPtr && fc_slot_startpoint < fc_startpoint)
	{
		List *fc_history;
		TimeLineID fc_slot_start_tli;
		TimeLineID	fc_currTLI;		
		if (!fc_backup_started_in_recovery)
			fc_currTLI = GetWALInsertionTimeLine();
		else
			(void)GetXLogReplayRecPtr(&fc_currTLI);

		/* 插槽所需的最小LSN可能在较早的时间线中。 */
		fc_history = readTimeLineHistory(fc_currTLI);
		fc_slot_start_tli = tliOfPointInHistory(fc_slot_startpoint, fc_history);
		list_free_deep(fc_history);

		if (fc_slot_start_tli < fc_starttli)
			fc_starttli = fc_slot_start_tli;

		fc_startpoint = fc_slot_startpoint;
	}
#endif
	/*
	 * 我们完成了。为了方便，返回起始WAL位置。
	 */
	if (fc_starttli_p)
		*fc_starttli_p = fc_starttli;
	return fc_startpoint;
}

/* pg_backup_start的错误清理回调 */
static void fc_pg_backup_start_callback(int fc_code, Datum fc_arg)
{
	/* 在失败时更新备份计数器并强制写入页面 */
	fc_WALInsertLockAcquireExclusive();

	Assert(XLogCtl->Insert.runningBackups > 0);
	XLogCtl->Insert.runningBackups--;

	if (XLogCtl->Insert.runningBackups == 0)
	{
		XLogCtl->Insert.forcePageWrites = false;
	}
	fc_WALInsertLockRelease();
}

/*
 * 实用程序例程，用于获取正在运行的备份的会话级状态。
 */
SessionBackupState
get_backup_status(void)
{
	return sessionBackupState;
}

/*
 * do_pg_backup_stop
 *
 * 在线备份结束时调用的实用程序函数。它清理备份状态，
 * 并可以选择等待WAL段被归档。
 *
 * 返回必须存在以从此备份恢复的最后WAL位置，
 * 以及对应的时间线ID在*stoptli_p中。
 *
 * 调用此函数的调用者有责任验证调用用户的权限！
 */
XLogRecPtr do_pg_backup_stop(char *fc_labelfile, bool fc_waitforarchive, TimeLineID *fc_stoptli_p)
{
	bool		fc_backup_started_in_recovery = false;
	XLogRecPtr	fc_startpoint;
	XLogRecPtr	fc_stoppoint;
	TimeLineID	fc_stoptli;
	pg_time_t	fc_stamp_time;
	char		fc_strfbuf[128];
	char		fc_histfilepath[MAXPGPATH];
	char		fc_startxlogfilename[MAXFNAMELEN];
	char		fc_stopxlogfilename[MAXFNAMELEN];
	char		fc_lastxlogfilename[MAXFNAMELEN];
	char		fc_histfilename[MAXFNAMELEN];
	char		fc_backupfrom[20];
	XLogSegNo	fc__logSegNo;
	FILE	   *fc_fp;
	char		fc_ch;
	int			fc_seconds_before_warning;
	int			fc_waits = 0;
	bool		fc_reported_waiting = false;
	char	   *fc_remaining;
	char	   *fc_ptr;
	uint32		fc_hi,
				fc_lo;

	fc_backup_started_in_recovery = RecoveryInProgress();

	/*
	 * 在恢复过程中，我们不需要检查 WAL 级别。因为，如果 WAL 级别不足，在恢复期间不可能到达这里。
	 */
	if (!fc_backup_started_in_recovery && !XLogIsNeeded())
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("WAL level not sufficient for making an online backup"),
				 errhint("wal_level must be set to \"replica\" or \"logical\" at server start.")));

	/*
	 * 可以更新备份计数器、forcePageWrites 和会话级锁。
	 *
	 * 注意，更新它们时不得调用 CHECK_FOR_INTERRUPTS()。
	 * 否则，它们可能会不一致地更新，这可能导致
	 * do_pg_abort_backup() 失败。
	 */
	fc_WALInsertLockAcquireExclusive();

	/*
	 * 预计每次 do_pg_backup_start() 调用都恰好匹配
	 * 一次 do_pg_backup_stop() 调用。
	 */
	Assert(XLogCtl->Insert.runningBackups > 0);
	XLogCtl->Insert.runningBackups--;

	if (XLogCtl->Insert.runningBackups == 0)
	{
		XLogCtl->Insert.forcePageWrites = false;
	}

	/*
	 * 清理会话级锁。
	 *
	 * 你可能认为可以在清理会话级锁之前调用
	 * WALInsertLockRelease()，因为会话级锁不需要
	 * 使用 WAL 插入锁进行保护。但由于
	 * CHECK_FOR_INTERRUPTS() 可能在其中发生，因此
	 * 必须在此之前清理会话级锁。
	 */
	sessionBackupState = SESSION_BACKUP_NONE;

	fc_WALInsertLockRelease();

	/*
	 * 读取并解析 START WAL LOCATION 行（这段代码比较粗糙，
	 * 但我们不期待文件格式有任何变化）。
	 */
	if (sscanf(fc_labelfile, "START WAL LOCATION: %X/%X (file %24s)%c",
			   &fc_hi, &fc_lo, fc_startxlogfilename,
			   &fc_ch) != 4 || fc_ch != '\n')
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("invalid data in file \"%s\"", BACKUP_LABEL_FILE)));
	fc_startpoint = ((uint64) fc_hi) << 32 | fc_lo;
	fc_remaining = strchr(fc_labelfile, '\n') + 1;	/* %n 的可移植性不足 */

	/*
	 * 解析 BACKUP FROM 行。如果我们从备用服务器进行在线备份，
	 * 我们确认在备份期间备用服务器没有被提升。
	 */
	fc_ptr = strstr(fc_remaining, "BACKUP FROM:");
	if (!fc_ptr || sscanf(fc_ptr, "BACKUP FROM: %19s\n", fc_backupfrom) != 1)
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("invalid data in file \"%s\"", BACKUP_LABEL_FILE)));
	if (strcmp(fc_backupfrom, "standby") == 0 && !fc_backup_started_in_recovery)
		ereport(ERROR,
				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
				 errmsg("the standby was promoted during online backup"),
				 errhint("This means that the backup being taken is corrupt "
						 "and should not be used. "
						 "Try taking another online backup.")));

	/*
	 * 在恢复期间，我们不写备份结束记录。我们假设
	 * pg_control 是最后备份的，并且其最小恢复点可以
	 * 作为备份结束位置使用。由于我们没有备份结束记录，
	 * 我们使用 pg_control 值来检查在从此
	 * 备份开始恢复时是否已达到备份结束。 然而我们无法检查
	 * pg_control 是否未被最后备份。
	 *
	 * 我们不强制切换到新的 WAL 文件，但如果 waitforarchive
	 * 为 true，仍然可能会等待所有所需文件归档。这没关系，
	 * 如果我们使用备份来启动一个备用服务器并通过流复制获取
	 * 缺失的 WAL。但在归档恢复的情况下，用户应该将
	 * waitforarchive 设置为 true，并等待它们被归档，以确保
	 * 所有所需文件都可用。
	 *
	 * 我们返回当前最小恢复点作为备份结束位置。请注意，
	 * 如果最小恢复点在 pg_control 备份后被更新，它可能大于
	 * 精确的备份结束位置。这对于当前使用来说是无害的。
	 *
	 * XXX 目前备份历史文件仅用于信息和调试目的。
	 * 对于在线备份不是必需的。此外，即使它被创建，
	 * 在恢复期间也不会归档，因为不会调用归档程序。
	 * 因此，在恢复期间写入备份历史文件似乎没有
	 * 价值。
	 */
	if (fc_backup_started_in_recovery)
	{
		XLogRecPtr	fc_recptr;

		/*
		 * 检查在在线备份期间重放的所有 WAL 是否包含
		 * 全页写入。
		 */
		SpinLockAcquire(&XLogCtl->info_lck);
		fc_recptr = XLogCtl->lastFpwDisableRecPtr;
		SpinLockRelease(&XLogCtl->info_lck);

		if (fc_startpoint <= fc_recptr)
			ereport(ERROR,
					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
					 errmsg("WAL generated with full_page_writes=off was replayed "
							"during online backup"),
					 errhint("This means that the backup being taken on the standby "
							 "is corrupt and should not be used. "
							 "Enable full_page_writes and run CHECKPOINT on the primary, "
							 "and then try an online backup again.")));


		LWLockAcquire(ControlFileLock, LW_SHARED);
		fc_stoppoint = ControlFile->minRecoveryPoint;
		fc_stoptli = ControlFile->minRecoveryPointTLI;
		LWLockRelease(ControlFileLock);
	}
	else
	{
		/*
		 * 写入备份结束的 xlog 记录
		 */
		XLogBeginInsert();
		XLogRegisterData((char *) (&fc_startpoint), sizeof(fc_startpoint));
		fc_stoppoint = XLogInsert(RM_XLOG_ID, XLOG_BACKUP_END);

		/*
		 * 鉴于我们不在恢复中，InsertTimeLineID 被设置并且不能
		 * 更改，因此我们可以在没有锁的情况下读取它。
		 */
		fc_stoptli = XLogCtl->InsertTimeLineID;

		/*
		 * 强制切换到新的 xlog 段文件，以便在归档程序移动当前段文件后备份立即有效。
		 */
		RequestXLogSwitch(false);

		XLByteToPrevSeg(fc_stoppoint, fc__logSegNo, wal_segment_size);
		XLogFileName(fc_stopxlogfilename, fc_stoptli, fc__logSegNo, wal_segment_size);

		/* 在这里使用日志时区，而不是会话时区 */
		fc_stamp_time = (pg_time_t) time(NULL);
		pg_strftime(fc_strfbuf, sizeof(fc_strfbuf),
					"%Y-%m-%d %H:%M:%S %Z",
					pg_localtime(&fc_stamp_time, log_timezone));

		/*
		 * 写入备份历史文件
		 */
		XLByteToSeg(fc_startpoint, fc__logSegNo, wal_segment_size);
		BackupHistoryFilePath(fc_histfilepath, fc_stoptli, fc__logSegNo,
							  fc_startpoint, wal_segment_size);
		fc_fp = AllocateFile(fc_histfilepath, "w");
		if (!fc_fp)
#ifdef FDD //cppcheck
		{
#endif
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not create file \"%s\": %m",
							fc_histfilepath)));
#ifdef FDD //cppcheck
			return 0;
		}
#endif
		fprintf(fc_fp, "START WAL LOCATION: %X/%X (file %s)\n",
				LSN_FORMAT_ARGS(fc_startpoint), fc_startxlogfilename);
		fprintf(fc_fp, "STOP WAL LOCATION: %X/%X (file %s)\n",
				LSN_FORMAT_ARGS(fc_stoppoint), fc_stopxlogfilename);

		/*
		 * 将剩余行（包括标签和起始时间线）传输到历史文件中。
		 */
		fprintf(fc_fp, "%s", fc_remaining);
		fprintf(fc_fp, "STOP TIME: %s\n", fc_strfbuf);
		fprintf(fc_fp, "STOP TIMELINE: %u\n", fc_stoptli);
		if (fflush(fc_fp) || ferror(fc_fp) || FreeFile(fc_fp))
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not write file \"%s\": %m",
							fc_histfilepath)));

		/*
		 * 清除任何不再需要的历史文件。作为副作用，这将为新创建的历史文件发布 .ready 文件，
		 * 通知归档程序可以立即归档历史文件。
		 */
		fc_CleanupBackupHistory();
	}

	/*
	 * 如果启用了归档，在返回之前等待所有必需的 WAL 文件被归档。如果没有启用归档，所需的 WAL
	 * 需要通过流复制方式传输（希望 wal_keep_size 设置得足够高），或者使用一些更复杂的机制，例如
	 * 轮询和从 pg_wal 复制文件的脚本。我们对这些机制不了解，因此用户需要确保他获取到所有
	 * 所需的 WAL。
	 *
	 * 我们等待直到备份期间填充的最后一个 WAL 文件和历史文件都被归档，并假设 WAL 文件的
	 * 字母排序特性确保任何较早的 WAL 文件也被安全地归档。
	 *
	 * 我们会一直等待，因为 archive_command 应该能够工作，我们假设管理员希望他的备份能顺利完成。
	 * 如果您不想等待，则可以将 waitforarchive 传入 false，或者可以设置 statement_timeout。
	 * 此外，还会发出一些通知，以提醒可能正在以交互方式进行此操作的任何人。
	 */

	if (fc_waitforarchive &&
		((!fc_backup_started_in_recovery && XLogArchivingActive()) ||
		 (fc_backup_started_in_recovery && XLogArchivingAlways())))
	{
		XLByteToPrevSeg(fc_stoppoint, fc__logSegNo, wal_segment_size);
		XLogFileName(fc_lastxlogfilename, fc_stoptli, fc__logSegNo, wal_segment_size);

		XLByteToSeg(fc_startpoint, fc__logSegNo, wal_segment_size);
		BackupHistoryFileName(fc_histfilename, fc_stoptli, fc__logSegNo,
							  fc_startpoint, wal_segment_size);

		fc_seconds_before_warning = 60;
		fc_waits = 0;

		while (XLogArchiveIsBusy(fc_lastxlogfilename) ||
			   XLogArchiveIsBusy(fc_histfilename))
		{
			CHECK_FOR_INTERRUPTS();

			if (!fc_reported_waiting && fc_waits > 5)
			{
				ereport(NOTICE,
						(errmsg("base backup done, waiting for required WAL segments to be archived")));
				fc_reported_waiting = true;
			}

			(void) WaitLatch(MyLatch,
							 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
							 1000L,
							 WAIT_EVENT_BACKUP_WAIT_WAL_ARCHIVE);
			ResetLatch(MyLatch);

			if (++fc_waits >= fc_seconds_before_warning)
			{
				fc_seconds_before_warning *= 2;	/* 这会持续超过 10 年... */
				ereport(WARNING,
						(errmsg("still waiting for all required WAL segments to be archived (%d seconds elapsed)",
								fc_waits),
						 errhint("Check that your archive_command is executing properly.  "
								 "You can safely cancel this backup, "
								 "but the database backup will not be usable without all the WAL segments.")));
			}
		}

		ereport(NOTICE,
				(errmsg("all required WAL segments have been archived")));
	}
	else if (fc_waitforarchive)
		ereport(NOTICE,
				(errmsg("WAL archiving is not enabled; you must ensure that all required WAL segments are copied through other means to complete the backup")));

	/*
	 * 我们完成了。为了方便起见，返回结束的 WAL 位置。
	 */
	if (fc_stoptli_p)
		*fc_stoptli_p = fc_stoptli;
	return fc_stoppoint;
}


/*
 * do_pg_abort_backup: 中止正在进行的备份
 *
 * 这只是执行 do_pg_backup_stop() 的最基本步骤，通过使系统退出备份模式，
 * 从而使从错误处理程序调用变得更安全。
 *
 * 调用者可以将 'arg' 传递为 'true' 或 'false' 来控制是否发出警告。
 *
 * 注意：这被用作 before_shmem_exit 处理程序，因此签名看起来有些奇怪。
 */
void do_pg_abort_backup(int fc_code, Datum fc_arg)
{
	bool		fc_emit_warning = DatumGetBool(fc_arg);

	/*
	 * 如果会话没有正在进行的备份，快速退出。
	 */
	if (sessionBackupState != SESSION_BACKUP_RUNNING)
		return;

	fc_WALInsertLockAcquireExclusive();
	Assert(XLogCtl->Insert.runningBackups > 0);
	XLogCtl->Insert.runningBackups--;

	if (XLogCtl->Insert.runningBackups == 0)
	{
		XLogCtl->Insert.forcePageWrites = false;
	}

	sessionBackupState = SESSION_BACKUP_NONE;
	fc_WALInsertLockRelease();

	if (fc_emit_warning)
		ereport(WARNING,
				(errmsg("aborting backup due to backend exiting before pg_backup_stop was called")));
}

/*
 * 注册一个处理程序，该处理程序将在会话结束时警告未终止的备份，除非已执行此操作。
 */
void register_persistent_abort_backup_handler(void)
{
	static bool fc_already_done = false;

	if (fc_already_done)
		return;
	before_shmem_exit(do_pg_abort_backup, DatumGetBool(true));
	fc_already_done = true;
}

/*
 * 获取最新的 WAL 插入指针
 */
XLogRecPtr GetXLogInsertRecPtr(void)
{
	XLogCtlInsert *fc_Insert = &XLogCtl->Insert;
	uint64		fc_current_bytepos;

	SpinLockAcquire(&fc_Insert->insertpos_lck);
	fc_current_bytepos = fc_Insert->CurrBytePos;
	SpinLockRelease(&fc_Insert->insertpos_lck);

	return fc_XLogBytePosToRecPtr(fc_current_bytepos);
}

/*
 * 获取最新的 WAL 写入指针
 */
XLogRecPtr GetXLogWriteRecPtr(void)
{
	SpinLockAcquire(&XLogCtl->info_lck);
	LogwrtResult = XLogCtl->LogwrtResult;
	SpinLockRelease(&XLogCtl->info_lck);

	return LogwrtResult.Write;
}

/*
 * 返回最后一个检查点或重启点的重做指针。如果我们必须重新启动恢复，这是
 * WAL 中我们仍然需要的最旧点。
 */
void GetOldestRestartPoint(XLogRecPtr *fc_oldrecptr, TimeLineID *fc_oldtli)
{
	LWLockAcquire(ControlFileLock, LW_SHARED);
	*fc_oldrecptr = ControlFile->checkPointCopy.redo;
	*fc_oldtli = ControlFile->checkPointCopy.ThisTimeLineID;
	LWLockRelease(ControlFileLock);
}

/* 对 ShutdownWalRcv() 的简化包装。 */
void XLogShutdownWalRcv(void)
{
	ShutdownWalRcv();

	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	XLogCtl->InstallXLogFileSegmentActive = false;
	LWLockRelease(ControlFileLock);
}

/* 启用 WAL 文件回收和预分配。 */
void SetInstallXLogFileSegmentActive(void)
{
	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
	XLogCtl->InstallXLogFileSegmentActive = true;
	LWLockRelease(ControlFileLock);
}

bool IsInstallXLogFileSegmentActive(void)
{
	bool		fc_result;

	LWLockAcquire(ControlFileLock, LW_SHARED);
	fc_result = XLogCtl->InstallXLogFileSegmentActive;
	LWLockRelease(ControlFileLock);

	return fc_result;
}

/*
 * 更新 WalWriterSleeping 标志。
 */
void SetWalWriterSleeping(bool fc_sleeping)
{
	SpinLockAcquire(&XLogCtl->info_lck);
	XLogCtl->WalWriterSleeping = fc_sleeping;
	SpinLockRelease(&XLogCtl->info_lck);
}
